function frmProFormJS() { /* globals frm_js, frmFrontForm, google, jQuery */ /* globals __frmDatepicker, __frmDropzone, __frmUniqueTimes */ /* globals __FRMLOOKUP, __FRMCALC, __FRMRULES, __FRMCURR */ /* globals __frmChosen, __frmHideOrShowFields, __frmDepDynamicFields */ /* globals __frmDepLookupFields, __frmMasks, __FRMTABLES */ /* globals frmCheckboxI18n */ 'use strict'; let currentlyAddingRow = false; let action = ''; let processesRunning = 0; const lookupQueues = {}; let hiddenSubmitButtons = []; let pendingDynamicFieldAjax = []; const frmMaskInstances = new Map(); const pendingLookupFieldAjax = []; let listWrappersOriginal = {}; const intlPhoneInputs = {}; let autoId = 0; function setNextPage( e ) { var closestButton; if ( this.className.includes('frm_rootline_title') ) { closestButton = this.previousElementSibling; closestButton.click(); return; } if ( this.className.includes('frm_rootline_single') ) { this.querySelector( 'input' ).click(); return; } /*jshint validthis:true */ var $thisObj = jQuery( this ); var thisType = $thisObj.attr( 'type' ); if ( thisType !== 'submit' ) { e.preventDefault(); } var f = $thisObj.parents( 'form' ).first(), v = '', d = '', thisName = this.name; if ( thisName === 'frm_prev_page' || this.className.includes('frm_prev_page') ) { v = jQuery( f ).find( '.frm_next_page' ).attr( 'id' ).replace( 'frm_next_p_', '' ); if ( f.length ) { maybeAddEmptyHiddenInputsForCheckboxes( f.get( 0 ) ); } } else if ( thisName === 'frm_save_draft' || this.className.includes('frm_save_draft') ) { d = 1; } else if ( this.className.includes('frm_page_skip') ) { var goingTo = $thisObj.data( 'page' ); var formId = jQuery( f ).find( 'input[name="form_id"]' ).val(); var orderField = jQuery( f ).find( 'input[name="frm_page_order_' + formId + '"]' ); jQuery( f ).append( '' ); if ( goingTo === '' ) { orderField.remove(); } else { orderField.val( goingTo ); } } else if ( this.className.includes('frm_page_back') ) { v = $thisObj.data( 'page' ); } if ( 1 === d ) { resetTinyMceOnDraftSave(); } else { resetTinyMceOnPageTurn(); } jQuery( '.frm_next_page' ).val( v ); jQuery( '.frm_saving_draft' ).val( d ); if ( thisType !== 'submit' ) { f.trigger( 'submit' ); } } /** * When no checkboxes are checked, no $_POST data is sent. * This causes issues when going back when drafts are enabled. * The draft gets saved when the next button is pressed, and then * gets reloaded after being unchecked because there is no $_POST data. * This adds a hidden input with no value in its place so $_POST data does get sent. * * @since 6.8.3 * * @param {HTMLElement} form * @returns {void} */ function maybeAddEmptyHiddenInputsForCheckboxes( form ) { form.querySelectorAll( '.frm_opt_container' ).forEach( ( optContainer ) => { var checkboxes, hiddenInput; checkboxes = optContainer.querySelectorAll( 'input[type="checkbox"]' ); if ( checkboxes.length && ! jQuery( checkboxes ).filter( ':checked' ).length ) { hiddenInput = document.createElement( 'input' ); hiddenInput.setAttribute( 'type', 'hidden' ); hiddenInput.setAttribute( 'name', checkboxes[ 0 ].getAttribute( 'name' ) ); optContainer.append( hiddenInput ); } } ); } function resetTinyMceOnDraftSave() { jQuery( document ).one( 'frmFormComplete', () => { jQuery( '.wp-editor-area' ).each( function() { reInitializeRichText( this.id ); }); }); } function resetTinyMceOnPageTurn() { var removeIds = []; jQuery( '.frm_form_field .wp-editor-area' ).each( function() { removeIds.push( this.id ); } ); jQuery( document ).one( 'frmPageChanged', () => { var removeIndex, removeId; // Remove tinymce from RTE fields on page update so they can initialize properly when the page becomes active again for ( removeIndex = 0; removeIndex < removeIds.length; ++removeIndex ) { removeId = removeIds[ removeIndex ]; removeRichText( removeId ); } checkConditionalLogic(); }); } function toggleSection( e ) { var $toggleContainer, togglingOn; if ( e.key !== undefined ) { if ( e.key !== ' ' && e.key !== 'Enter' ) { return; } } else if ( e.keyCode !== undefined && e.keyCode !== 32 && e.keyCode !== 13 ) { return; } e.preventDefault(); /*jshint validthis:true */ $toggleContainer = jQuery( this ).parent().children( '.frm_toggle_container' ); togglingOn = 'none' === $toggleContainer.get( 0 ).style.display; if ( togglingOn ) { $toggleContainer.show(); } triggerEvent( document, 'frmBeforeToggleSection', { toggleButton: this }); if ( togglingOn ) { $toggleContainer.hide(); } $toggleContainer.slideToggle( 'fast' ); if ( togglingOn ) { this.className += ' active'; this.setAttribute( 'aria-expanded', 'true' ); } else { this.className = this.className.replace( ' active', '' ); this.setAttribute( 'aria-expanded', 'false' ); } } function loadDateFields() { jQuery( document ).on( 'focusin', '.frm_date', triggerDateField ); document.addEventListener( 'focusout', keepFloatingLabelOnDateBlur, true ); loadUniqueTimeFields(); } function frmDatepickerPro( input, settings ) { if ( input._flatpickr ) { return input._flatpickr; } const _this = this; // Init the static methods this.initAccessiblity = frmDatepickerPro.initAccessiblity; this.initMonthSelector = frmDatepickerPro.initMonthSelector; this.initYearSelector = frmDatepickerPro.initYearSelector; this.parseFunctionConfig = frmDatepickerPro.parseFunctionConfig; this.getDateRange = frmDatepickerPro.getDateRange; this.callbacks = frmDatepickerPro.callbacks; this.getDefaultConfigs = frmDatepickerPro.getDefaultConfigs; this.isUsingACustomTheme = frmDatepickerPro.isUsingACustomTheme; this.getConfigs = function() { if ( 'undefined' === typeof settings.datepickerJsOptions ) { return _this.getDefaultConfigs( settings, input ); } return { ..._this.getDefaultConfigs( settings, input ), ..._this.parseFunctionConfig( settings.datepickerJsOptions ) }; }; return flatpickr( input, this.getConfigs() ); } frmDatepickerPro.getThemeType = function() { const themeFile = document.querySelector( 'link[href*="flatpickr/dist/themes/"]' ); const themeSelectors = [ { name: 'frm-dark', selector: 'dark.css' }, { name: 'frm-material-blue', selector: 'material_blue.css' }, { name: 'frm-material-green', selector: 'material_green.css' }, { name: 'frm-material-orange', selector: 'material_orange.css' }, { name: 'frm-material-red', selector: 'material_red.css' }, { name: 'frm-airbnb', selector: 'airbnb.css' }, { name: 'frm-confetti', selector: 'confetti.css' } ]; if ( themeFile && themeFile.href ) { for ( const theme of themeSelectors ) { if ( themeFile.href.includes( theme.selector ) ) { return theme.name; } } } return ''; }; frmDatepickerPro.themeType = frmDatepickerPro.getThemeType(); frmDatepickerPro.isUsingACustomTheme = frmDatepickerPro.themeType !== ''; frmDatepickerPro.callbacks = {}; frmDatepickerPro.callbacks.onOpen = function( selectedDates, dateStr, instance ) { instance.calendarContainer.classList.add( 'frm-datepicker', 'with_frm_style' ); if ( frmDatepickerPro.isUsingACustomTheme ) { instance.calendarContainer.classList.add( 'frm-datepicker-custom-theme', frmDatepickerPro.themeType ); } }; frmDatepickerPro.callbacks.onClose = function( selectedDates, dateStr, instance ) { if ( ! instance.config.inline ) { instance.calendarContainer.classList.remove( 'frm-datepicker', 'with_frm_style' ); if ( frmDatepickerPro.isUsingACustomTheme ) { instance.calendarContainer.classList.remove( 'frm-datepicker-custom-theme', frmDatepickerPro.themeType ); } } triggerEvent( instance.input, 'frmFlatpickrClosed', { instance, }); }; /** * Get the default configs for the datepicker. * * @since 6.19 * * @returns {object} */ frmDatepickerPro.getDefaultConfigs = function( config, input ) { this.getSettings = function() { const result = {}; if ( config.formidable_dates || config.options ) { return config; } result.formidable_dates = config; result.options = config.datepickerOptions; return result; }; this.getDatesDisabled = function( settings ) { const disabledDates = settings?.formidable_dates?.datesDisabled || settings?.options?.datesDisabled; if ( ! Array.isArray( disabledDates ) ) { return []; } return disabledDates.map( date => new Date( date + 'T00:00:00' ) ); }; this.getInstanceElement = ( instance ) => { if ( instance.config.inline && 'INPUT' !== instance.element.nodeName ) { const fieldId = instance.element.dataset.fieldId; const element = document.querySelector( `input[name="item_meta[${ fieldId }]"]` ); if ( element ) { return element; } return instance.element; } return instance.element; }; /** * Update the range fields(start field and the related end field) on change. * * @since 6.24 * * @param {string} mode The mode of the datepicker. * @param {object} instance The instance of the datepicker. * @param {string} dateStr The date string. * @param {array} selectedDates The selected dates. * @returns {void} */ this.updateRangeFieldsOnChange = ( mode, instance, dateStr, selectedDates ) => { if ( 'range' !== mode ) { return; } // The pair is keyed by the start field's id. On the end field that is // data-range-start-field-id, so it must win over the field's own id. const fieldId = input.dataset.rangeStartFieldId || input.dataset.fieldId; const startDateField = document.querySelector( `input[data-field-id="${ fieldId }"]` ) || document.querySelector( `input[type="hidden"][name="item_meta[${ fieldId }]"]` ); const [ startDate, endDate ] = selectedDates; const endDateField = 'undefined' !== typeof endDate ? document.querySelector( `input[data-range-start-field-id="${ fieldId }"]` ) : null; const instanceElement = this.getInstanceElement( instance ); instanceElement.dataset.rangeValue = dateStr; if ( instanceElement === startDateField && null !== endDateField ) { instanceElement.value = flatpickr.formatDate( startDate, settings.options.fpDateFormat ); endDateField.dataset.rangeValue = dateStr; endDateField._flatpickr.setDate( dateStr ); endDateField.value = flatpickr.formatDate( endDate, settings.options.fpDateFormat ); // Trigger the change event to end range field to update the calculation endDateField.dispatchEvent( new Event( 'change', { bubbles: true } ) ); } if ( instanceElement === endDateField ) { instanceElement.value = flatpickr.formatDate( endDate, settings.options.fpDateFormat ); startDateField.dataset.rangeValue = dateStr; startDateField._flatpickr.setDate( dateStr ); startDateField.value = flatpickr.formatDate( startDate, settings.options.fpDateFormat ); } }; const settings = this.getSettings(); const mode = input.classList.contains( 'frm_date_range' ) || ( settings.formidable_dates && settings.formidable_dates.isRangeEnabled ) ? 'range' : 'single'; const inline = settings.formidable_dates && settings.formidable_dates.inline; const showMonths = inline && 'range' === mode ? 2 : 1; const { min, max } = frmDatepickerPro.getDateRange( settings ); const disabledDates = this.getDatesDisabled( settings ); const firstDay = settings.options.firstDay || 1; const flatpickrOptions = { dateFormat: settings.options.fpDateFormat, inline: settings.formidable_dates && settings.formidable_dates.inline, minDate: min, maxDate: max, monthSelectorType: 'true' === settings.options.changeMonth ? 'dropdown' : 'static', changeYear: settings.options.changeYear, closeOnSelect: true, showMonths, locale: { ...flatpickr.l10ns[ settings.locale || settings.options.locale ], firstDayOfWeek: firstDay, }, mode, disable: [ function( date ) { return settings.formidable_dates && settings.formidable_dates.daysEnabled && -1 === settings.formidable_dates.daysEnabled.indexOf( date.getDay() ); }, ...( disabledDates ? disabledDates : [] ) ], onReady( selectedDates, dateStr, instance ) { setTimeout( () => frmDatepickerPro.setDefaultRangeValue( instance.element ), 0 ); if ( instance.config.inline ) { instance.calendarContainer.classList.add( 'frm-datepicker', 'with_frm_style', 'frm_date_inline' ); if ( frmDatepickerPro.isUsingACustomTheme ) { instance.calendarContainer.classList.add( 'frm-datepicker-custom-theme', frmDatepickerPro.themeType ); } } frmDatepickerPro.initMonthSelector( instance ); frmDatepickerPro.initYearSelector( instance ); frmDatepickerPro.initAccessiblity( instance ); }, onChange: ( selectedDates, dateStr, instance ) => { if ( instance.config.inline ) { const altInput = instance.config.altInputElement || instance.altInput; if ( altInput ) { altInput.value = dateStr; altInput.dispatchEvent( new Event( 'change', { bubbles: true } ) ); } } this.updateRangeFieldsOnChange( mode, instance, dateStr, selectedDates ); }, onOpen( selectedDates, dateStr, instance ) { frmDatepickerPro.callbacks.onOpen( selectedDates, dateStr, instance ); }, onClose: frmDatepickerPro.callbacks.onClose, shorthandCurrentMonth: true, altInputClass: '' }; if ( settings.formidable_dates && settings.formidable_dates.inline ) { flatpickrOptions.altInput = true; flatpickrOptions.altInputElement = document.querySelector( settings.options.altField ); } return flatpickrOptions; }; /** * Set the default range value for the datepicker. * * @since 6.24 * * @param {object} input * @returns {void} */ frmDatepickerPro.setDefaultRangeValue = function( instanceElement ) { let input = instanceElement; if ( instanceElement._flatpickr.config.inline ) { const fieldId = instanceElement.dataset.fieldId; input = document.querySelector( `input[name="item_meta[${ fieldId }]"]` ); if ( ! input ) { const container = instanceElement.closest( '.frm_repeat_sec, .frm_repeat_inline, .frm_repeat_grid' ); if ( container ) { input = container.querySelector( `input[name^="item_meta[${ fieldId }]["]` ); } } if ( ! input ) { return; } input.dataset.fieldId = fieldId; } if ( ! input.classList.contains( 'frm_date_range' ) ) { return null; } const startDate = input.dataset.rangeStartFieldId ? document.querySelector( `input[data-field-id="${ input.dataset.rangeStartFieldId }"]` ) : input; const startDateValue = startDate.value; const endDate = input.dataset.rangeStartFieldId ? input : document.querySelector( `input[data-range-start-field-id="${ input.dataset.fieldId }"]` ); const endDateValue = endDate.value; if ( instanceElement._flatpickr.config.inline ) { instanceElement._flatpickr.setDate( startDateValue + ' to ' + endDateValue ); startDate.value = startDateValue; endDate.value = endDateValue; return; } // When the inline datepicker is enabled, the range’s default value is controlled by the “Start Date” field. // If the Flatpickr instance for “Start Date” isn’t available, setDefaultRangeValue is executed for the “End Date” field when the "Display Inline" is activated - the default dates are already updated by the "Start Date" field if ( 'undefined' === typeof startDate._flatpickr ) { return; } startDate._flatpickr.setDate( startDateValue + ' to ' + endDateValue ); startDate.value = startDateValue; endDate._flatpickr.setDate( startDateValue + ' to ' + endDateValue ); endDate.value = endDateValue; }; /** * Initialize the month selector for the datepicker. * * @since 6.19 * * @param {object} instance * @returns {void} */ frmDatepickerPro.initMonthSelector = function( instance ) { if ( 'static' !== instance.config.monthSelectorType ) { return; } instance.calendarContainer.classList.add( 'frm-date-no-month-select' ); }; /** * Initialize the year selector for the datepicker. * * @since 6.19 * * @param {object} instance * @returns {void} */ frmDatepickerPro.initYearSelector = function( instance ) { if ( 'false' !== instance.config.changeYear ) { return; } instance.calendarContainer.classList.add( 'frm-date-no-year-select' ); }; /** * Initialize the accessibility for the datepicker. * * @since 6.19 * * @param {object} instance * @returns {void} */ frmDatepickerPro.initAccessiblity = function( instance ) { /** * Initialize the accessibility for the datepicker. * * @since 6.19 * * @returns {void} */ this.init = function() { instance.calendarContainer.setAttribute( 'role', 'dialog' ); instance.calendarContainer.setAttribute( 'tabindex', '0' ); this.prevButton(); this.nextButton(); }; /** * Initialize the previous button for the datepicker. * * @since 6.19 * * @returns {void} */ this.prevButton = function( ) { const prevArrow = instance.calendarContainer.querySelector( '.flatpickr-prev-month' ); if ( null === prevArrow ) { return; } prevArrow.setAttribute( 'tabindex', '0' ); prevArrow.setAttribute( 'role', 'button' ); prevArrow.addEventListener( 'keydown', ( event ) => { if ( event.key === 'Enter' ) { prevArrow.click(); } }); }; /** * Initialize the next button for the datepicker. * * @since 6.19 * * @returns {void} */ this.nextButton = function( ) { const nextArrow = instance.calendarContainer.querySelector( '.flatpickr-next-month' ); if ( null === nextArrow ) { return; } nextArrow.setAttribute( 'tabindex', '0' ); nextArrow.setAttribute( 'role', 'button' ); nextArrow.addEventListener( 'keydown', ( event ) => { if ( event.key === 'Enter' ) { nextArrow.click(); } }); }; this.init(); }; /** * Get the date range for the datepicker. * * @since 6.19 * * @param {object} optionsData * @returns {object} */ frmDatepickerPro.getDateRange = function( optionsData ) { const dates = { min: null, max: null, }; const [ minYear, maxYear ] = optionsData.options.yearRange.split( ':' ); dates.min = new Date( minYear + '-01-01T00:00:00' ); dates.max = new Date( maxYear + '-12-31T23:59:59' ); if ( ! optionsData.formidable_dates ) { return dates; } if ( optionsData.formidable_dates.maximum_date_cond ) { if ( optionsData.options.maxDate ) { dates.max = optionsData.options.maxDate; } else { dates.max = frmDatepickerPro.parseOffsetDate( optionsData.formidable_dates.maximum_date_val ); } } if ( optionsData.formidable_dates.minimum_date_cond ) { if ( optionsData.options.minDate ) { dates.min = optionsData.options.minDate; } else { dates.min = frmDatepickerPro.parseOffsetDate( optionsData.formidable_dates.minimum_date_val ); } } return dates; }; /** * Parse the offset date from the datepickerJsOptions. * * @since 6.19 * * @param {string} date * @returns {object} */ frmDatepickerPro.parseOffsetDate = function( date ) { const cleanedDate = date.toLowerCase().replace(/\s+/g, ''); const regex = /^([+-]\d+)(day|days|month|months|year|years)$/; const match = cleanedDate.match( regex ); if ( ! match ) { return date; } const [, number, unit ] = match; const amount = parseInt( number ); const currentDate = new Date(); if ( unit.startsWith( 'day' ) ) { currentDate.setDate( currentDate.getDate() + amount ); return currentDate; } if ( unit.startsWith( 'month' ) ) { currentDate.setMonth( currentDate.getMonth() + amount ); return currentDate; } if ( unit.startsWith( 'year' ) ) { currentDate.setFullYear( currentDate.getFullYear() + amount ); return currentDate; } return currentDate; }; /** * Parse the function config from the datepickerJsOptions. * * @since 6.19 * * @param {string} config * @returns {object} */ frmDatepickerPro.parseFunctionConfig = function( config ) { const parsed = JSON.parse( config ); const stringToFunction = ( str ) => { if ( typeof str === 'string' && str.startsWith( 'function' ) ) { return new Function( 'return ' + str )(); } return str; }; Object.keys( parsed ).forEach( key => { if ( Array.isArray( parsed[ key ] ) ) { parsed[ key ] = parsed[ key ].map( item => stringToFunction( item ) ); } else { parsed[ key ] = stringToFunction( parsed[ key ] ); } }); return parsed; }; /** * Check if the flatpickr library is available. * * @since 6.19 */ frmDatepickerPro.useFlatpickr = () => window.frm_js && window.frm_js.datepickerLibrary === 'flatpickr'; function triggerDateField() { /*jshint validthis:true */ if ( this.className.includes('frm_custom_date') || typeof __frmDatepicker === 'undefined' ) { return; } var dateFields = __frmDatepicker, id = this.id, idParts = id.split( '-' ), altID = ''; if ( isRepeatingFieldByName( this.name ) ) { altID = 'input[id^="' + idParts[ 0 ] + '"]'; } else { altID = 'input[id^="' + idParts.join( '-' ) + '"]'; } var optKey = 0; for ( var i = 0; i < dateFields.length; i++ ) { if ( dateFields[ i ].triggerID === '#' + id || dateFields[ i ].triggerID == altID ) { optKey = i; break; } } if ( dateFields[ optKey ].options.defaultDate !== '' ) { dateFields[ optKey ].options.defaultDate = new Date( dateFields[ optKey ].options.defaultDate ); } if ( frmDatepickerPro.useFlatpickr() ) { new frmDatepickerPro( this, dateFields[ optKey ] ); } else { // Legacy datepicker. // TO DO: Remove this once the legacy datepicker is removed. dateFields[ optKey ].options.beforeShow = frmProForm.addFormidableClassToDatepicker; dateFields[ optKey ].options.onClose = frmProForm.removeFormidableClassFromDatepicker; jQuery( this ).datepicker( jQuery.extend( {}, jQuery.datepicker.regional[ dateFields[ optKey ].locale ], dateFields[ optKey ].options )); } } function loadDropzones( repeatRow ) { if ( typeof __frmDropzone === 'undefined' ) { return; } var uploadFields = __frmDropzone; for ( var i = 0; i < uploadFields.length; i++ ) { loadDropzone( i, repeatRow ); } } function loadDropzone( i, repeatRow ) { var field, max, uploadedCount, form, uploadFields = __frmDropzone, uploadField = uploadFields[ i ], selector = '#' + uploadField.htmlID + '_dropzone', fieldName = uploadField.fieldName; let label = uploadField.label; if ( repeatRow !== undefined && selector.includes('-0_dropzone') ) { selector = selector.replace( '-0_dropzone', '-' + repeatRow + '_dropzone' ); fieldName = fieldName.replace( '[0]', '[' + repeatRow + ']' ); label = label.replace( /-0$/, '-' + repeatRow ); delete uploadField.mockFiles; } field = jQuery( selector ); if ( field.length < 1 || field.hasClass( 'dz-clickable' ) || field.hasClass( 'dz-started' ) ) { return; } max = uploadField.maxFiles; if ( uploadField.mockFiles !== undefined ) { uploadedCount = uploadField.mockFiles.length; if ( max > 0 ) { max = max - uploadedCount; } } form = field.closest( 'form' ); uploadField = uploadFields[ i ]; const dropzoneConfig = { url: getAjaxUrl( form.get( 0 ) ), headers: { 'Frm-Dropzone': 1 }, addRemoveLinks: false, paramName: field.attr( 'id' ).replace( '_dropzone', '' ), maxFilesize: uploadField.maxFilesize, minFilesize: uploadField.minFilesize, maxFiles: max, uploadMultiple: uploadField.uploadMultiple, hiddenInputContainer: field.parent()[0], dictDefaultMessage: uploadField.defaultMessage, dictFallbackMessage: uploadField.fallbackMessage, dictFallbackText: uploadField.fallbackText, dictFileTooBig: uploadField.fileTooBig, dictFileTooSmall: uploadField.fileTooSmall, dictInvalidFileType: uploadField.invalidFileType, dictResponseError: uploadField.responseError, dictCancelUpload: uploadField.cancel, dictCancelUploadConfirmation: uploadField.cancelConfirm, dictRemoveFile: uploadField.remove, dictMaxFilesExceeded: uploadField.maxFilesExceeded, resizeMethod: 'contain', resizeWidth: uploadField.resizeWidth, resizeHeight: uploadField.resizeHeight, thumbnailWidth: 60, thumbnailHeight: 60, timeout: uploadField.timeout, previewTemplate: filePreviewHTML( uploadField ), acceptedFiles: uploadField.acceptedFiles, fallback() { // Force ajax submit to turn off jQuery( this.element ).closest( 'form' ).removeClass( 'frm_ajax_submit' ); }, init() { var hidden, mockFileIndex, mockFileData, mockFile; hidden = field.parent().find( '.dz-hidden-input' ); if ( hidden.attr( 'id' ) === undefined ) { hidden.attr( 'id', label ); } this.on( 'thumbnail', function( file ) { if ( file.size < 1024 * 1024 * ( this.options.minFilesize ) ) { if ( 'function' === typeof file.rejectSize ) { file.rejectSize(); } } }); this.on( 'sending', function( file, xhr, formData ) { if ( isSpam( uploadFields[ i ].parentFormID, uploadField.checkHoneypot ) ) { this.removeFile( file ); alert( frm_js.file_spam ); return false; } formData.append( 'action', 'frm_submit_dropzone' ); formData.append( 'field_id', uploadFields[ i ].fieldID ); formData.append( 'form_id', uploadFields[ i ].formID ); formData.append( 'nonce', frm_js.nonce ); if ( form.get( 0 ).hasAttribute( 'data-token' ) ) { formData.append( 'antispam_token', form.get( 0 ).getAttribute( 'data-token' ) ); } }); this.on( 'processing', function() { if ( ! this.options.uploadMultiple ) { this.removeEventListeners(); } }); this.on( 'success', function( file, response ) { var mediaIDs, m, mediaID; mediaIDs = JSON.parse( response ); for ( m = 0; m < mediaIDs.length; m++ ) { if ( uploadFields[ i ].uploadMultiple !== true ) { mediaID = mediaIDs[ m ]; jQuery( 'input[name="' + fieldName + '"]' ).val( mediaID ); } } if ( this.options.uploadMultiple === false ) { this.disable(); } // Clear the errors here so it only happens on upload. // complete and addedfile both get triggered when reloading a form with a file ID. clearErrorsOnUpload( file.previewElement ); }); this.on( 'successmultiple', ( files, response ) => { var mediaIDs = JSON.parse( response ); for ( var m = 0; m < files.length; m++ ) { jQuery( files[ m ].previewElement ).append( getHiddenUploadHTML( uploadFields[ i ], mediaIDs[ m ], fieldName ) ); } }); this.on( 'complete', ( file ) => { var fileName, node, img, thumbnail; processesRunning--; if ( form.length ) { removeSubmitLoading( form.get( 0 ), uploadFields[ i ].formID, processesRunning ); } if ( file.mediaID === undefined ) { return; } if ( uploadFields[ i ].uploadMultiple ) { jQuery( file.previewElement ).append( getHiddenUploadHTML( uploadFields[ i ], file.mediaID, fieldName ) ); } // Add download link to the file fileName = file.previewElement.querySelectorAll( '[data-dz-name]' ); for ( var _i = 0, _len = fileName.length; _i < _len; _i++ ) { node = fileName[ _i ]; if ( file.accessible ) { node.innerHTML = '' + file.name + ''; } else { node.innerHTML = file.name; } if ( file.ext ) { img = file.previewElement.querySelector( '.dz-image img' ); if ( null !== img ) { thumbnail = maybeGetExtensionThumbnail( file.ext ); if ( false !== thumbnail ) { img.setAttribute( 'src', thumbnail ); } } } } }); this.on( 'addedfile', ( file ) => { var ext, thumbnail; ext = file.name.split( '.' ).pop(); thumbnail = maybeGetExtensionThumbnail( ext ); processesRunning++; frmFrontForm.showSubmitLoading( form ); if ( false !== thumbnail ) { jQuery( file.previewElement ).find( '.dz-image img' ).attr( 'src', thumbnail ); } }); function clearErrorsOnUpload( fileElement ) { var container = fileElement.closest( '.frm_form_field' ); if ( ! container ) { return; } container.classList.remove( 'frm_blank_field', 'has-error' ); container.querySelectorAll( '.form-field .frm_error, .frm_error_style' ).forEach( ( error ) => { if ( error.parentNode ) { error.parentNode.removeChild( error ); } } ); } this.on( 'removedfile', function( file ) { var fileCount = this.files.length; if ( this.options.uploadMultiple === false && fileCount < 1 ) { this.enable(); } if ( file.accepted !== false && uploadFields[ i ].uploadMultiple !== true ) { jQuery( 'input[name="' + fieldName + '"]' ).val( '' ); } if ( file.accepted !== false && file.mediaID !== undefined ) { jQuery( file.previewElement ).remove(); fileCount = this.files.length; this.options.maxFiles = uploadFields[ i ].maxFiles - fileCount; } }); if ( uploadFields[ i ].mockFiles !== undefined ) { for ( mockFileIndex = 0; mockFileIndex < uploadFields[ i ].mockFiles.length; mockFileIndex++ ) { mockFileData = uploadFields[ i ].mockFiles[ mockFileIndex ]; mockFile = { name: mockFileData.name, size: mockFileData.size, url: mockFileData.file_url, mediaID: mockFileData.id, accessible: mockFileData.accessible, ext: mockFileData.ext, type: mockFileData.type }; this.emit( 'addedfile', mockFile ); if ( mockFile.accessible && 'string' === typeof mockFile.type && 0 === mockFile.type.indexOf( 'image/' ) ) { this.emit( 'thumbnail', mockFile, mockFileData.url ); } this.emit( 'complete', mockFile ); this.files.push( mockFile ); } } }, accept( file, done ) { var message = this.options.dictFileTooSmall.replace( '{{minFilesize}}', this.options.minFilesize ); file.rejectSize = function() { done( message ); }; return done(); } }; /** * Fixes Chrome Android bug where cloud storage files (Google Drive, etc.) fail to upload. * * @see https://bugs.chromium.org/p/chromium/issues/detail?id=1063576 */ if ( /Android/i.test( navigator.userAgent ) && /Chrome/i.test( navigator.userAgent ) ) { /** * Transforms a file before Dropzone uploads it on Android Chrome. * * @param {File} file The file selected by the user. * @param {Function} done Callback invoked with the file or Blob to upload. * @returns {void} */ dropzoneConfig.transformFile = function( file, done ) { /** * Forwards the file through Dropzone's resize when configured for images. * * @param {File|Blob} fileToUpload The file or in-memory Blob ready to upload. * @returns {void} */ const maybeResize = ( fileToUpload ) => { if ( ( this.options.resizeWidth || this.options.resizeHeight ) && fileToUpload.type && fileToUpload.type.match( /image.*/ ) ) { this.resizeImage( fileToUpload, this.options.resizeWidth, this.options.resizeHeight, this.options.resizeMethod, done ); return; } done( fileToUpload ); }; const reader = new FileReader(); reader.onload = () => { const blob = new Blob( [ reader.result ], { type: file.type } ); blob.name = file.name; blob.lastModified = file.lastModified; maybeResize( blob ); }; reader.onerror = () => maybeResize( file ); reader.readAsArrayBuffer( file ); }; } field.dropzone( dropzoneConfig ); } /** * Load imask for the given container. * * @since 6.23 * * @param {string|undefined} containerId * @returns {void} */ function loadImask( containerId ) { if ( ! window.IMask ) { return; } let inputs; if ( containerId ) { inputs = document.querySelectorAll( '#' + containerId + ' input[data-frmmask]' ); } else { inputs = document.querySelectorAll( 'input[data-frmmask]' ); } inputs.forEach( ( input ) => { if ( input.getAttribute( 'data-frm-imask-initialized' ) ) { return; } input.setAttribute( 'data-frm-imask-initialized', 1 ); const mask = IMask( input, { mask: input.dataset.frmmask, lazy: true, definitions: { '*': /[a-zA-Z0-9]/ } } ); // Store the mask instance for later access frmMaskInstances.set( input, mask ); input.addEventListener( 'change', () => mask.updateValue() ); // Show placeholder on focus input.addEventListener( 'focus', () => { mask.updateOptions({ lazy: false }); // Extend the maxlength by 1 to fix an issue where the placeholder characters would prevent // characters from being added. if ( input.hasAttribute( 'maxlength' ) && input.value.length >= input.maxLength ) { if ( ! input.hasAttribute( 'original-maxlength' ) ) { input.setAttribute( 'original-maxlength', input.maxLength ); } input.maxLength = parseInt( input.getAttribute( 'original-maxlength' ) ) + 1; } if ( '' === mask.unmaskedValue ) { // This fixes an issue in Chrome, where it // automatically would move the cursor to the end of the input. mask.alignCursor( 0 ); } } ); input.addEventListener( 'blur', () => { input.value = input.value.trim(); if ( '' === mask.unmaskedValue ) { mask.updateOptions({ lazy: true }); } if ( input.hasAttribute( 'original-maxlength' ) ) { input.maxLength = parseInt( input.getAttribute( 'original-maxlength' ) ); } } ); } ); } /** * Remove the submit loading class from a form and * only enable the submit button if it is not conditionally disabled. * * @param {HTMLElement} form * @param {string} formId * @param {number} processesRunning * @returns {void} */ function removeSubmitLoading( form, formId, processesRunning ) { var isFinalSubmitButton, enable; isFinalSubmitButton = form.querySelector( '.frm_submit .frm_button_submit.frm_final_submit' ); enable = ! isFinalSubmitButton || ! submitButtonIsConditionallyDisabled( formId ) ? 'enable' : ''; if ( '' === enable ) { // If "enable" is not passed, the save draft button stays disabled. // This code enables the save draft button. jQuery( '.frm_loading_form' ).find( 'a.frm_save_draft' ).css( 'pointer-events', '' ); } frmFrontForm.removeSubmitLoading( jQuery( form ), enable, processesRunning ); } /** * Check if the submit button is conditionally disabled. * This is required for Stripe link so the button does not get enabled at the wrong time after completing the Stripe elements. * * @since 6.7.1 * * @param {String} formId * @returns {bool} */ function submitButtonIsConditionallyDisabled( formId ) { return submitButtonIsConditionallyNotAvailable( formId ) && 'disable' === __FRMRULES[ 'submit_' + formId ].hideDisable; } /** * Check submit button is conditionally "hidden". This is also used for the enabled check and is used in submitButtonIsConditionallyDisabled. * * @since 6.7.1 * * @param {String} formId * @returns bool */ function submitButtonIsConditionallyNotAvailable( formId ) { var hideFields = document.getElementById( 'frm_hide_fields_' + formId ); return hideFields && -1 !== hideFields.value.indexOf( '"frm_form_' + formId + '_container .frm_final_submit"' ); } function maybeGetExtensionThumbnail( ext ) { if ( -1 !== [ 'jpg', 'jpeg', 'png' ].indexOf( ext ) ) { return false; } if ( 'pdf' === ext ) { return getProPluginUrl() + '/images/pdf.svg'; } if ( -1 !== ext.indexOf( 'xls' ) ) { return getProPluginUrl() + '/images/xls.svg'; } return getProPluginUrl() + '/images/doc.svg'; } function getProPluginUrl() { var freePluginUrlSplitBySlashes = frm_js.images_url.split( '/' ); freePluginUrlSplitBySlashes.pop(); freePluginUrlSplitBySlashes.pop(); freePluginUrlSplitBySlashes.push( 'formidable-pro' ); return freePluginUrlSplitBySlashes.join( '/' ); } function filePreviewHTML( field ) { return '
\n' + '
\n' + '
\n' + '
\n' + '
\n' + ' ' + // add white space between file name and file size. '
\n' + '' + '' + '' + '
\n' + '
\n' + '
\n' + '
\n' + '
'; } function getHiddenUploadHTML( field, mediaID, fieldName ) { return ''; } function removeFile() { /*jshint validthis:true */ var fieldName = jQuery( this ).data( 'frm-remove' ); fadeOut( jQuery( this ).closest( '.dz-preview' ) ); var singleField = jQuery( 'input[name="' + fieldName + '"]' ); if ( singleField.length ) { singleField.val( '' ); } } function postToAjaxUrl( form, data, success, error, extraParams ) { var ajaxParams = 'object' === typeof extraParams ? extraParams : {}; ajaxParams.type = 'POST'; ajaxParams.url = getAjaxUrl( form ); ajaxParams.data = data; ajaxParams.success = success; if ( 'function' === typeof error ) { ajaxParams.error = error; } jQuery.ajax( ajaxParams ); } function getAjaxUrl( form ) { var ajaxUrl, action; ajaxUrl = frm_js.ajax_url; action = form.getAttribute( 'action' ); if ( 'string' === typeof action && -1 !== action.indexOf( '?action=frm_forms_preview' ) ) { ajaxUrl = action.split( '?action=frm_forms_preview' )[0]; } return ajaxUrl; } function isSpam( formID, checkHoneypot ) { if ( isHeadless() ) { return true; } return checkHoneypot && isHoneypotSpam( formID ); } /** * Check if submission is Honeypot spam * * @since 2.03.08 * * @returns {boolean} */ function isHoneypotSpam( formID ) { var honeypotField = document.getElementById( 'frm_email_' + formID ); if ( honeypotField === null ) { honeypotField = document.getElementById( 'frm_form_' + formID + '_container' )?.querySelector( '.frm_verify[id^="field_"]' ); } return honeypotField !== null && honeypotField.value !== ''; } function isHeadless() { return ( window._phantom || window.callPhantom || //phantomjs window.__phantomas || //PhantomJS-based web perf metrics window.Buffer || //nodejs window.emit || //couchjs window.spawn //rhino ); } /** * Show "Other" text box when Other item is checked/selected * Hide and clear text box when item is unchecked/unselected */ function showOtherText() { /*jshint validthis:true */ var type = this.type, other = false, select = false; // Dropdowns if ( type === 'select-one' ) { select = true; var curOpt = this.options[ this.selectedIndex ]; if ( curOpt !== undefined && curOpt.className === 'frm_other_trigger' ) { other = true; } } else if ( type === 'select-multiple' ) { select = true; var allOpts = this.options; other = false; for ( var i = 0; i < allOpts.length; i++ ) { if ( allOpts[ i ].className === 'frm_other_trigger' ) { if ( allOpts[ i ].selected ) { other = true; break; } } } } if ( select ) { var otherField = jQuery( this ).parent().children( '.frm_other_input' ); if ( otherField.length ) { if ( other ) { // Remove frm_pos_none otherField[ 0 ].className = otherField[ 0 ].className.replace( 'frm_pos_none', '' ); } else { // Add frm_pos_none if ( otherField[ 0 ].className.indexOf( 'frm_pos_none' ) < 1 ) { otherField[ 0 ].className = otherField[ 0 ].className + ' frm_pos_none'; } otherField[ 0 ].value = ''; } } // Radio } else if ( type === 'radio' ) { if ( jQuery( this ).is( ':checked' ) ) { jQuery( this ).closest( '.frm_radio' ).children( '.frm_other_input' ).removeClass( 'frm_pos_none' ); jQuery( this ).closest( '.frm_radio' ).siblings().children( '.frm_other_input' ).addClass( 'frm_pos_none' ).val( '' ); } // Checkboxes } else if ( type === 'checkbox' ) { if ( this.checked ) { jQuery( this ).closest( '.frm_checkbox' ).children( '.frm_other_input' ).removeClass( 'frm_pos_none' ); } else { jQuery( this ).closest( '.frm_checkbox' ).children( '.frm_other_input' ).addClass( 'frm_pos_none' ).val( '' ); } } } function setToggleAriaChecked() { this.nextElementSibling.setAttribute( 'aria-checked', this.checked ? 'true' : 'false' ); } function maybeCheckDependent( _, field, fieldId, e ) { var $field, originalEvent; $field = jQuery( field ); checkFieldsWithConditionalLogicDependentOnThis( fieldId, $field ); originalEvent = getOriginalEvent( e ); checkFieldsWatchingLookup( fieldId, $field, originalEvent ); doCalculation( fieldId, $field ); } function getOriginalEvent( e ) { var originalEvent; if ( e.originalEvent !== undefined || e.currentTarget.className.includes('frm_chzn') || e.currentTarget.className.includes('frm_slimselect') ) { originalEvent = 'value changed'; } else { originalEvent = 'other'; } return originalEvent; } /***************************************************** Conditional Logic Functions ******************************************************/ /** * Check if a changed field has other fields depending on it. */ function checkFieldsWithConditionalLogicDependentOnThis( fieldId, changedInput ) { if ( typeof __FRMRULES === 'undefined' || __FRMRULES[ fieldId ] === undefined || __FRMRULES[ fieldId ].dependents.length < 1 || changedInput === null || changedInput === undefined ) { return; } var triggerFieldArgs = __FRMRULES[ fieldId ]; var repeatArgs = getRepeatArgsFromFieldName( changedInput[ 0 ].name ); pendingDynamicFieldAjax = []; for ( var i = 0, l = triggerFieldArgs.dependents.length; i < l; i++ ) { hideOrShowFieldById( triggerFieldArgs.dependents[ i ], repeatArgs ); } processPendingAjax(); } function processPendingAjax() { var fieldsToProcess, postData, data, formId, form; if ( ! pendingDynamicFieldAjax.length ) { return; } // Copy pendingDynamicFieldAjax as it gets reset to [] every time checkFieldsWithConditionalLogicDependentOnThis is called. fieldsToProcess = pendingDynamicFieldAjax.slice(); postData = []; for ( data in fieldsToProcess ) { postData.push( fieldsToProcess[ data ].data ); } formId = fieldsToProcess[0].args.depFieldArgs.formId; function processDynamicField( html, depFieldArgs, onCurrentPage ) { var $fieldDiv, $optContainer, $listInputs, listVal; if ( onCurrentPage ) { $fieldDiv = jQuery( '#' + depFieldArgs.containerId ); addLoadingIcon( $fieldDiv ); $optContainer = $fieldDiv.find( '.frm_opt_container, .frm_data_container' ); $optContainer.html( html ); $listInputs = $optContainer.children( 'input' ); listVal = $listInputs.val(); removeLoadingIcon( $optContainer ); if ( '' === html || '' === listVal ) { hideDynamicField( depFieldArgs ); } else { showDynamicField( depFieldArgs, $fieldDiv, $listInputs, true ); } } else { updateHiddenDynamicListField( depFieldArgs, html ); } } function ajaxHandler( response ) { var i; for ( i = 0; i < fieldsToProcess.length; i++ ) { processDynamicField( 'undefined' === typeof response[ i ] ? '' : response[ i ], fieldsToProcess[ i ].args.depFieldArgs, fieldsToProcess[ i ].args.onCurrentPage ); } } form = getFormById( formId ); if ( form ) { postToAjaxUrl( form, { action: 'frm_fields_ajax_get_data_arr', postData }, ajaxHandler, ( response ) => { console.error( response ); }, { dataType: 'json' } ); } } /** * Hide or show all instances of a field using the field ID * * @param fieldId * @param triggerFieldRepeatArgs */ function hideOrShowFieldById( fieldId, triggerFieldRepeatArgs ) { var depFieldArgs = getRulesForSingleField( fieldId ); if ( depFieldArgs === false || depFieldArgs.conditions.length < 1 ) { // If field has no logic on it, stop now return; } var childFieldDivIds = getAllFieldDivIds( depFieldArgs, triggerFieldRepeatArgs ); var childFieldNum = childFieldDivIds.length; for ( var i = 0; i < childFieldNum; i++ ) { depFieldArgs.containerId = childFieldDivIds[ i ]; addRepeatRow( depFieldArgs, childFieldDivIds[ i ]); hideOrShowSingleField( depFieldArgs ); } } /** * Get all the field divs that should be hidden or shown, regardless of whether they're on the current page * * @param {Object} depFieldArgs * @param {bool} depFieldArgs.isRepeating * @param {string} depFieldArgs.fieldId * @param {object} triggerFieldArgs * @param {string} triggerFieldArgs.repeatingSection * @param {string} triggerFieldArgs.repeatRow * @param {string} depFieldArgs.inEmbedForm * @returns {Array} */ function getAllFieldDivIds( depFieldArgs, triggerFieldArgs ) { var childFieldDivs = []; if ( depFieldArgs.isRepeating ) { if ( triggerFieldArgs.repeatingSection !== '' ) { // If trigger field is repeating/embedded, use its section row in selector var container = 'frm_field_' + depFieldArgs.fieldId + '-'; container += triggerFieldArgs.repeatingSection + '-' + triggerFieldArgs.repeatRow + '_container'; childFieldDivs.push( container ); } else { // If trigger field is not repeating/embedded, get all repeating/embedded field divs childFieldDivs = getAllRepeatingFieldDivIds( depFieldArgs ); } } else if ( depFieldArgs.fieldType === 'submit' ) { childFieldDivs.push( getSubmitButtonContainerID( depFieldArgs ) ); } else { childFieldDivs.push( 'frm_field_' + depFieldArgs.fieldId + '_container' ); } return childFieldDivs; } /** * Return selector for submit button for use in Conditional Logic. * * @param depFieldArgs * @returns {string} */ function getSubmitButtonContainerID( depFieldArgs ) { return 'frm_form_' + depFieldArgs.formId + '_container .frm_final_submit'; } /** * Get all instances of a repeating field * * @since 2.01.0 * @param {Object} depFieldArgs * @param {string} depFieldArgs.fieldId */ function getAllRepeatingFieldDivIds( depFieldArgs ) { var childFieldDivs = [], containerFieldId = getContainerFieldId( depFieldArgs ); // TODO: what if section is inside embedded form? if ( isFieldDivOnPage( 'frm_field_' + containerFieldId + '_container' ) ) { childFieldDivs = getRepeatingFieldDivIdsOnCurrentPage( depFieldArgs.fieldId ); } else { childFieldDivs = getRepeatingFieldDivIdsAcrossPage( depFieldArgs ); } return childFieldDivs; } /** * Get all repeating field divs on the current page * * @since 2.02.06 * @param string fieldId * @returns {Array} */ function getRepeatingFieldDivIdsOnCurrentPage( fieldId ) { var childFieldDivs = [], childFields = document.querySelectorAll( '.frm_field_' + fieldId + '_container' ); for ( var i = 0, l = childFields.length; i < l; i++ ) { childFieldDivs.push( childFields[ i ].id ); } return childFieldDivs; } /** * Get the field divs for repeating fields across a page * * @param {Object} depFieldArgs * @param {string} depFieldArgs.fieldId * @returns {Array} */ function getRepeatingFieldDivIdsAcrossPage( depFieldArgs ) { var childFieldDivs = [], containerFieldId = getContainerFieldId( depFieldArgs ), fieldDiv = 'frm_field_' + depFieldArgs.fieldId + '-' + containerFieldId + '-', allRows = document.querySelectorAll( '[name="item_meta[' + containerFieldId + '][row_ids][]"]' ); for ( var i = 0, l = allRows.length; i < l; i++ ) { if ( allRows[ i ].value !== '' ) { childFieldDivs.push( fieldDiv + allRows[ i ].value + '_container' ); } } if ( childFieldDivs.length < 1 ) { childFieldDivs.push( fieldDiv + '0_container' ); } return childFieldDivs; } /** * * @param {Object} depFieldArgs * @param {string} depFieldArgs.inSection * @param {string} depFieldArgs.inEmbedForm * @returns {string} */ function getContainerFieldId( depFieldArgs ) { var containerFieldId = ''; if ( depFieldArgs.inEmbedForm !== '0' ) { containerFieldId = depFieldArgs.inEmbedForm; } else if ( depFieldArgs.inSection !== '0' ) { containerFieldId = depFieldArgs.inSection; } return containerFieldId; } /** * @param depFieldArgs * @param {bool} depFieldArgs.isRepeating * @param childFieldDivId */ function addRepeatRow( depFieldArgs, childFieldDivId ) { if ( depFieldArgs.isRepeating ) { var divParts = childFieldDivId.replace( '_container', '' ).split( '-' ); depFieldArgs.repeatRow = divParts[ 2 ]; } else { depFieldArgs.repeatRow = ''; } } /** * @param {object} depFieldArgs * @return {void} */ function hideOrShowSingleField( depFieldArgs ) { var i, add, logicOutcomes = [], len = depFieldArgs.conditions.length; for ( i = 0; i < len; i++ ) { add = checkLogicCondition( depFieldArgs.conditions[ i ], depFieldArgs ); if ( add !== null ) { // Prevent fields not on the page from being hidden without a chance. logicOutcomes.push( add ); } } if ( logicOutcomes.length ) { // Only continue if the field was found on the page. routeToHideOrShowField( depFieldArgs, logicOutcomes ); } } function getRulesForSingleField( fieldId ) { if ( typeof __FRMRULES === 'undefined' || __FRMRULES[ fieldId ] === undefined ) { return false; } return __FRMRULES[ fieldId ]; } /** * @param {Array} logicCondition * @param {string} logicCondition.fieldId * @param depFieldArgs * @returns {*} */ function checkLogicCondition( logicCondition, depFieldArgs ) { var fieldId = logicCondition.fieldId, logicFieldArgs = getRulesForSingleField( fieldId ), fieldValue = getFieldValue( logicFieldArgs, depFieldArgs ); if ( fieldValue === null ) { // The field wasn't found. return null; } return getLogicConditionOutcome( logicCondition, fieldValue, depFieldArgs, logicFieldArgs ); } /** * Get the value from any field. * * @param {object} logicFieldArgs * @param {string} logicFieldArgs.inputType * @param {object} depFieldArgs * @returns {string} */ function getFieldValue( logicFieldArgs, depFieldArgs ) { var fieldValue = ''; if ( 'name' === logicFieldArgs.fieldType ) { fieldValue = getValueFromNameField( logicFieldArgs, depFieldArgs ); } else if ( logicFieldArgs.inputType === 'radio' || logicFieldArgs.inputType === 'checkbox' || logicFieldArgs.inputType === 'toggle' ) { fieldValue = getValueFromRadioOrCheckbox( logicFieldArgs, depFieldArgs ); } else if ( logicFieldArgs.inputType === 'date' ) { fieldValue = getValueForDateField( logicFieldArgs, depFieldArgs ); } else { fieldValue = getValueFromTextOrDropdown( logicFieldArgs, depFieldArgs ); } fieldValue = cleanFinalFieldValue( fieldValue ); return fieldValue; } /** * @since 6.21 * * @param {object} logicFieldArgs * @param {object} depFieldArgs * @return {string} */ function getValueForDateField( logicFieldArgs, depFieldArgs ) { const fieldId = logicFieldArgs.fieldId; const fieldContainer = document.getElementById( 'frm_field_' + fieldId + '_container' ); if ( fieldContainer && fieldContainer.querySelector( '.frm_date_inline' ) ) { const altField = fieldContainer.querySelector( '#field_' + logicFieldArgs.fieldKey + '_alt' ); if ( altField ) { return altField.value; } } return getValueFromTextOrDropdown( logicFieldArgs, depFieldArgs ); } function getValueFromNameField( logicFieldArgs, depFieldArgs ) { var inputName, inputs, nameValues; inputName = buildLogicFieldInputName( logicFieldArgs, depFieldArgs ); inputs = document.querySelectorAll( '[name^="' + inputName + '"]' ); nameValues = []; inputs.forEach( ( input ) => { nameValues.push( input.value ); } ); return nameValues.join( ' ' ); } /** * Get the value from a Text or Dropdown field * * @param logicFieldArgs * @param {string} logicFieldArgs.fieldKey * @param {bool} logicFieldArgs.isRepeating * @param {bool} logicFieldArgs.isMultiSelect * @param depFieldArgs * @param {string} depFieldArgs.repeatRow */ function getValueFromTextOrDropdown( logicFieldArgs, depFieldArgs ) { var logicFieldValue = ''; if ( logicFieldArgs.isMultiSelect === true ) { return getValueFromMultiSelectDropdown( logicFieldArgs, depFieldArgs ); } var fieldCall = 'field_' + logicFieldArgs.fieldKey; if ( logicFieldArgs.isRepeating ) { // If trigger field is repeating, dependent field is repeating too fieldCall += '-' + depFieldArgs.repeatRow; } var logicFieldInput = document.getElementById( fieldCall ); if ( logicFieldInput === null ) { logicFieldValue = parseTimeValue( logicFieldArgs, fieldCall ); if ( logicFieldValue === '' ) { // Check for hidden fields. logicFieldValue = getValueFromMultiSelectDropdown( logicFieldArgs, depFieldArgs ); } } else { logicFieldValue = logicFieldInput.value; } return logicFieldValue; } function parseTimeValue( logicFieldArgs, fieldCall ) { var logicFieldValue = ''; if ( logicFieldArgs.fieldType === 'time' ) { var hour = document.getElementById( fieldCall + '_H' ); if ( hour !== null ) { var minute = document.getElementById( fieldCall + '_m' ); logicFieldValue = hour.value + ':' + minute.value; var pm = document.getElementById( fieldCall + '_A' ); if ( logicFieldValue == ':' ) { logicFieldValue = ''; } else if ( pm !== null ) { logicFieldValue += ' ' + pm.value; } } } return logicFieldValue; } function getValueFromMultiSelectDropdown( logicFieldArgs, depFieldArgs ) { var inputName = buildLogicFieldInputName( logicFieldArgs, depFieldArgs ), logicFieldInputs = document.querySelectorAll( '[name^="' + inputName + '"]' ), selectedVals = []; // TODO: What about if it's read-only? if ( logicFieldInputs.length == 1 && logicFieldInputs[ 0 ].type !== 'hidden' ) { selectedVals = jQuery( '[name^="' + inputName + '"]' ).val(); if ( selectedVals === null ) { selectedVals = ''; } } else { selectedVals = getValuesFromCheckboxInputs( logicFieldInputs ); } return selectedVals; } /** * Get the value from a Radio or Checkbox field trigger field * * @param {Object} logicFieldArgs * @param {string} logicFieldArgs.inputType * @param {Object} depFieldArgs * @returns {String|Array} */ function getValueFromRadioOrCheckbox( logicFieldArgs, depFieldArgs ) { var logicFieldValue, inputName = buildLogicFieldInputName( logicFieldArgs, depFieldArgs ), logicFieldInputs = document.querySelectorAll( 'input[name^="' + inputName + '"]' ); if ( logicFieldInputs.length === 0 ) { // Don't continue if the field doesn't exist. return null; } if ( logicFieldArgs.inputType === 'checkbox' || logicFieldArgs.inputType === 'toggle' ) { logicFieldValue = getValuesFromCheckboxInputs( logicFieldInputs ); } else { logicFieldValue = getValueFromRadioInputs( logicFieldInputs ); } return logicFieldValue; } /** * Build a logic field's input name * Does not include full name for checkbox, address, or multi-select fields * * @param {object} logicFieldArgs * @param {boolean} logicFieldArgs.isRepeating * @param {string} logicFieldArgs.fieldId * @param {object} depFieldArgs * @param {string} depFieldArgs.inEmbedForm * @param {string} depFieldArgs.inSection * @param {string} depFieldArgs.repeatRow * @returns {string} */ function buildLogicFieldInputName( logicFieldArgs, depFieldArgs ) { var inputName = ''; if ( logicFieldArgs.isRepeating ) { // If the trigger field is repeating, the child must be repeating as well var sectionId = ''; if ( depFieldArgs.inEmbedForm !== '0' ) { sectionId = depFieldArgs.inEmbedForm; } else { sectionId = depFieldArgs.inSection; } var rowId = depFieldArgs.repeatRow; inputName = 'item_meta[' + sectionId + '][' + rowId + '][' + logicFieldArgs.fieldId + ']'; } else { inputName = 'item_meta[' + logicFieldArgs.fieldId + ']'; } return inputName; } function getValuesFromCheckboxInputs( inputs ) { var checkedVals = []; for ( var i = 0, l = inputs.length; i < l; i++ ) { if ( inputs[ i ].type === 'hidden' || inputs[ i ].checked ) { checkedVals.push( inputs[ i ].value ); } else if ( inputs[ i ].dataset.off !== undefined ) { checkedVals.push( inputs[ i ].dataset.off ); } } if ( checkedVals.length === 0 ) { checkedVals = false; } return checkedVals; } function cleanFinalFieldValue( fieldValue ) { if ( fieldValue === undefined ) { fieldValue = ''; } else if ( typeof fieldValue === 'string' ) { fieldValue = fieldValue.trim(); } return fieldValue; } /** * Check whether a particular conditional logic condition is true or false * * @param {Array} logicCondition * @param {operator:string} logicCondition.operator * @param {value:string} logicCondition.value * @param {String|Array} fieldValue * @param {Object} depFieldArgs * @param {string} depFieldArgs.fieldType * @param {Object} logicFieldArgs * @param {fieldType:string} logicFieldArgs.fieldType * @returns {Boolean} */ function getLogicConditionOutcome( logicCondition, fieldValue, depFieldArgs, logicFieldArgs ) { var outcome; if ( depFieldArgs.fieldType === 'data' && logicFieldArgs.fieldType === 'data' ) { // If dep field is Dynamic and logic field is Dynamic outcome = getDynamicFieldLogicOutcome( logicCondition, fieldValue, depFieldArgs ); } else { const values = maybePrepareValuesCompared( logicCondition, fieldValue, depFieldArgs.formId ); outcome = operators( logicCondition.operator, values.logicValue, values.fieldValue ); } return outcome; } /** * @param {Array} logicCondition * @param {string} logicCondition.operator * @param {string} logicCondition.value * @param {string|Array} fieldValue * @param {object} depFieldArgs * @param {Array} depFieldArgs.dataLogic * @returns {boolean} */ function getDynamicFieldLogicOutcome( logicCondition, fieldValue, depFieldArgs ) { var outcome = false; if ( logicCondition.value === '' ) { // Logic: "Dynamic field is equal to/not equal to anything" if ( fieldValue === '' || ( fieldValue.length == 1 && fieldValue[ 0 ] === '' ) ) { outcome = false; } else { outcome = true; } } else { const values = maybePrepareValuesCompared( logicCondition, fieldValue, depFieldArgs.formId ); // Logic: "Dynamic field is equal to/not equal to specific option" outcome = operators( logicCondition.operator, values.logicValue, values.fieldValue ); } depFieldArgs.dataLogic = logicCondition; depFieldArgs.dataLogic.actualValue = fieldValue; return outcome; } /** * @since 6.19 * * @param {object} logicCondition * @param {number|string} fieldValue * @param {string} formId * @returns {object} */ function maybePrepareValuesCompared( logicCondition, fieldValue, formId ) { let logicValue = logicCondition.value; if ( 'undefined' === typeof __FRMRULES || 'undefined' === typeof __FRMRULES[ logicCondition.fieldId ] ) { return { logicValue, fieldValue }; } const logicConditionFieldType = __FRMRULES[ logicCondition.fieldId ].fieldType; if ( 'total' === logicConditionFieldType ) { const currency = getCurrency( formId ); logicValue = preparePrice( logicCondition.value, currency ); fieldValue = preparePrice( fieldValue, currency ); } else if ( 'date' === logicConditionFieldType ) { const dateFormat = getDateFormatForField( logicCondition.fieldId ); if ( false !== dateFormat ) { logicValue = prepareDate( logicValue, dateFormat ); fieldValue = prepareDate( fieldValue, dateFormat ); } } return { logicValue, fieldValue }; } /** * Change date to Y-m-d format if the current format is not sortable. * * @since 6.21 * * @param {string} date * @param {string} format * @return {string} */ function prepareDate( date, format ) { if ( 'Y-m-d' === format ) { return date; } const possibleSeparators = [ '/', '.', '-' ]; let separator = false; possibleSeparators.forEach( ( sep ) => { if ( format.includes( sep ) ) { separator = sep; } } ); if ( separator === false ) { return date; } const split = date.split( separator ); if ( 3 !== split.length ) { return date; } let yearPart, monthPart, dayPart; switch ( format ) { case 'Y/m/d': yearPart = split[ 0 ]; monthPart = split[ 1 ]; dayPart = split[ 2 ]; break; case 'n/j/Y': case 'm/d/Y': monthPart = split[ 0 ]; dayPart = split[ 1 ]; yearPart = split[ 2 ]; break; case 'd/m/Y': case 'd.m.Y': case 'j/m/Y': case 'j/n/Y': case 'j-m-Y': default: dayPart = split[ 0 ]; monthPart = split[ 1 ]; yearPart = split[ 2 ]; break; } if ( yearPart.length !== 4 ) { return date; } if ( monthPart.length < 2 ) { monthPart = '0' + monthPart; } if ( dayPart.length < 2 ) { dayPart = '0' + dayPart; } return yearPart + '-' + monthPart + '-' + dayPart; } /** * @since 6.21 * * @param {string} fieldId * @return {string|false} */ function getDateFormatForField( fieldId ) { if ( ! window.__frmDatepicker ) { return false; } let fieldRule = false; __frmDatepicker.forEach( ( rule ) => { if ( ! rule.fieldId || rule.fieldId === fieldId ) { fieldRule = rule; } } ); if ( false === fieldRule ) { return false; } return fieldRule.options.fpDateFormat; } function operators( op, a, b ) { var theOperators; a = prepareLogicValueForComparison( a ); b = prepareEnteredValueForComparison( a, b ); if ( typeof a === 'string' && a.indexOf( '"' ) != '-1' && operators( op, a.replace( '"', '"' ), b ) ) { return true; } theOperators = { '=='( c, d ) { return c === d; }, '!='( c, d ) { return c !== d; }, '<'( c, d ) { return c > d; }, '<='( c, d ) { return c >= d; }, '>'( c, d ) { return c < d; }, '>='( c, d ) { return c <= d; }, 'LIKE'( c, d ) { if ( ! d ) { /* If no value, then assume no match */ return false; } c = prepareLogicValueForLikeComparison( c ); d = prepareEnteredValueForLikeComparison( c, d ); return d.includes(c); }, 'not LIKE'( c, d ) { if ( ! d ) { /* If no value, then assume no match */ return true; } c = prepareLogicValueForLikeComparison( c ); d = prepareEnteredValueForLikeComparison( c, d ); return !d.includes(c); }, // Starts with 'LIKE%'( c, d ) { if ( ! d ) { /* If no value, then assume no match */ return false; } c = prepareLogicValueForLikeComparison( c ); d = prepareEnteredValueForLikeComparison( c, d ); if ( Array.isArray( d ) ) { // If d is still an array after calling prepareEnteredValueForLikeComparison, there is no match. return false; } return d.substr( 0, c.length ) === c; }, // Ends with '%LIKE'( c, d ) { if ( ! d ) { /* If no value, then assume no match */ return false; } c = prepareLogicValueForLikeComparison( c ); d = prepareEnteredValueForLikeComparison( c, d ); if ( Array.isArray( d ) ) { // If d is still an array after calling prepareEnteredValueForLikeComparison, there is no match. return false; } return d.substr( -c.length ) === c; } }; if ( 'function' !== typeof theOperators[ op ]) { op = '=='; // Fallback to Equals to. } return theOperators[ op ]( a, b ); } function prepareLogicValueForComparison( a ) { if ( shouldParseFloat( a ) ) { a = parseFloat( a ); } else if ( typeof a === 'string' ) { a = a.trim(); } return a; } /** * @param {string} value * @returns {bool} */ function shouldParseFloat( value ) { return String( value ).search( /^\s*(\+|-)?((\d+(\.\d+)?)|(\.\d+))\s*$/ ) !== -1; } function prepareEnteredValueForComparison( a, b ) { if ( b === undefined || b === null || b === false ) { b = ''; } if ( Array.isArray( b ) && jQuery.inArray( String( a ), b ) > -1 ) { b = a; } if ( typeof a === 'number' && typeof b === 'string' && shouldParseFloat( b ) ) { b = parseFloat( b ); } if ( typeof b === 'string' ) { b = b.trim(); } return b; } function prepareLogicValueForLikeComparison( val ) { return prepareValueForLikeComparison( val ); } function prepareEnteredValueForLikeComparison( logicValue, enteredValue ) { enteredValue = prepareValueForLikeComparison( enteredValue ); var currentValue = ''; if ( Array.isArray( enteredValue ) ) { for ( var i = 0, l = enteredValue.length; i < l; i++ ) { currentValue = enteredValue[ i ].toLowerCase(); if ( currentValue.includes(logicValue) ) { enteredValue = logicValue; break; } } } return enteredValue; } function prepareValueForLikeComparison( val ) { if ( typeof val === 'string' ) { val = val.toLowerCase(); } else if ( typeof val === 'number' ) { val = val.toString(); } return val; } /** * * @param {Object} depFieldArgs * @param {string} depFieldArgs.containerId * @param {string} depFieldArgs.fieldType * @param logicOutcomes */ function routeToHideOrShowField( depFieldArgs, logicOutcomes ) { var onCurrentPage, action = getHideOrShowAction( depFieldArgs, logicOutcomes ); if ( depFieldArgs.fieldType === 'submit' ) { onCurrentPage = isSubmitButtonOnPage( depFieldArgs.containerId ); } else { onCurrentPage = isFieldDivOnPage( depFieldArgs.containerId ); } if ( action === 'show' ) { if ( depFieldArgs.fieldType === 'data' && depFieldArgs.hasOwnProperty( 'dataLogic' ) ) { // Only update dynamic field options/value if it is dependent on another Dynamic field updateDynamicField( depFieldArgs, onCurrentPage ); } else { showFieldAndSetValue( depFieldArgs, onCurrentPage ); } } else { hideFieldAndClearValue( depFieldArgs, onCurrentPage ); } } function isFieldDivOnPage( containerId ) { var fieldDiv = document.getElementById( containerId ); return fieldDiv !== null; } /** * Checks if specified submit button is on the current page. * * @param depFieldArgs * @returns {boolean} */ function isSubmitButtonOnPage( container ) { var submitButton = document.querySelector( '#' + container ); return submitButton != null; } /** * @param {Object} depFieldArgs * @param {string} depFieldArgs.anyAll * @param {string} depFieldArgs.showHide * @param {Array} logicOutcomes * @returns {string} */ function getHideOrShowAction( depFieldArgs, logicOutcomes ) { if ( depFieldArgs.anyAll === 'any' ) { // If any of the following match logic if ( logicOutcomes.includes(true) ) { action = depFieldArgs.showHide; } else { action = reverseAction( depFieldArgs.showHide ); } } else { // If all of the following match logic if ( logicOutcomes.includes(false) ) { action = reverseAction( depFieldArgs.showHide ); } else { action = depFieldArgs.showHide; } } return action; } function reverseAction( action ) { if ( action === 'show' ) { action = 'hide'; } else { action = 'show'; } return action; } /** * @param {Object} depFieldArgs * @param {string} depFieldArgs.containerId * @param {string} depFieldArgs.formId * @param {bool} onCurrentPage */ function showFieldAndSetValue( depFieldArgs, onCurrentPage ) { if ( isFieldCurrentlyShown( depFieldArgs.containerId, depFieldArgs.formId ) ) { return; } removeFromHideFields( depFieldArgs.containerId, depFieldArgs.formId ); if ( depFieldArgs.fieldType === 'submit' ) { if ( onCurrentPage ) { showOrEnableSubmitButton( depFieldArgs ); } return; } if ( onCurrentPage ) { // Set value, then show field setValuesInsideFieldOnPage( depFieldArgs.containerId, depFieldArgs ); showFieldContainer( depFieldArgs.containerId ); triggerEvent( document, 'frmShowField' ); if ( depFieldArgs.inputType === 'rte' ) { reInitializeRichText( 'field_' + depFieldArgs.fieldKey ); } } else { setValuesInsideFieldAcrossPage( depFieldArgs ); } } /** * @param {string} fieldId */ function reInitializeRichText( fieldId ) { var isVisible = 'undefined' !== typeof tinyMCE.editors[ fieldId ] && ! tinyMCE.editors[ fieldId ].isHidden(); if ( ! isVisible ) { return; } removeRichText( fieldId ); initRichText( fieldId ); } /** * Show or enable submit button, as appropriate based on submit Conditional Logic. * * @param depFieldArgs */ function showOrEnableSubmitButton( depFieldArgs ) { if ( depFieldArgs.hideDisable && depFieldArgs.hideDisable === 'disable' ) { enableButton( '#' + depFieldArgs.containerId ); } else { showFieldContainer( depFieldArgs.containerId ); } removeSubmitButtonFromHiddenList( depFieldArgs ); } /** * Remove submit button from list of hidden submit buttons on page * * @param depFieldArgs */ function removeSubmitButtonFromHiddenList( depFieldArgs ) { hiddenSubmitButtons = hiddenSubmitButtons.filter( ( button ) => { return button !== depFieldArgs.formKey; }); } /** * Enable button with given selector. * * @param {string} buttonSelector * * @returns {void} */ function enableButton( buttonSelector ) { var button = document.querySelector( buttonSelector ); if ( button && ! button.closest( '.frm_loading_form' ) ) { button.disabled = false; } } /** * Set the value for all inputs inside of a field div on the current page * * @param {string} container * @param {Object} depFieldArgs * @param {string} depFieldArgs.fieldType * @param {string} depFieldArgs.formId */ function setValuesInsideFieldOnPage( container, depFieldArgs ) { var inputs = getInputsInFieldOnPage( container ), inContainer = ( depFieldArgs.fieldType === 'divider' || depFieldArgs.fieldType === 'form' ); setValueForInputs( inputs, inContainer, depFieldArgs.formId, 'required' ); } /** * Set the value for all inputs inside of a field across a page * * @param {Object} depFieldArgs * @param {string} depFieldArgs.fieldType * @param {string} depFieldArgs.formId */ function setValuesInsideFieldAcrossPage( depFieldArgs ) { var inputs = getInputsInFieldAcrossPage( depFieldArgs ), inContainer = depFieldArgs.fieldType === 'divider' || depFieldArgs.fieldType === 'form' || depFieldArgs.isRepeating; setValueForInputs( inputs, inContainer, depFieldArgs.formId ); } /** * Gets inputs in field on page. * * @since 5.4 Accepted HTML element as param. * * @param {String|HTMLElement} containerId ID of container or container element. * @return {Array} */ function getInputsInFieldOnPage( containerId ) { var container = 'string' === typeof containerId ? document.getElementById( containerId ) : containerId; return container.querySelectorAll( 'select[name^="item_meta"], textarea[name^="item_meta"], input[name^="item_meta"]' ); } /** * @param {Object} depFieldArgs * @param {string} depFieldArgs.fieldType * @returns {Array} */ function getInputsInFieldAcrossPage( depFieldArgs ) { var inputs = []; if ( depFieldArgs.fieldType === 'divider' ) { inputs = getInputsInHiddenSection( depFieldArgs ); } else if ( depFieldArgs.fieldType === 'form' ) { inputs = getInputsInHiddenEmbeddedForm( depFieldArgs ); } else { inputs = getHiddenInputs( depFieldArgs ); } return inputs; } /** * Get the inputs for a non-repeating field that is type=hidden * @param {object} depFieldArgs * @param {bool} depFieldArgs.isRepeating * @param {string} depFieldArgs.inSection * @param {string} depFieldArgs.repeatRow * @param {string} depFieldArgs.fieldId * @returns {NodeList} */ function getHiddenInputs( depFieldArgs ) { var name = ''; if ( depFieldArgs.isRepeating ) { //item_meta[section-id][row-id][field-id] var containerFieldId = getContainerFieldId( depFieldArgs ); name = 'item_meta[' + containerFieldId + '][' + depFieldArgs.repeatRow + '][' + depFieldArgs.fieldId + ']'; } else { // item_meta[field-id] name = 'item_meta[' + depFieldArgs.fieldId + ']'; } return document.querySelectorAll( '[name^="' + name + '"]' ); } function setValueForInputs( inputs, inContainer, formId, setRequired ) { var input, prevInput, i; if ( ! inputs.length ) { return; } for ( i = 0; i < inputs.length; i++ ) { input = inputs[ i ]; // Don't set the value if the field is in a section and it's conditionally hidden if ( inContainer && isChildInputConditionallyHidden( input, formId ) ) { continue; } if ( setRequired === 'required' ) { maybeAddRequiredTag( input ); } if ( skipSetValue( i, prevInput, inputs ) ) { continue; } setDefaultValue( input, inContainer ); maybeSetWatchingFieldValue( input ); setShownProduct( input ); maybeDoCalcForSingleField( input ); prevInput = input; } } /** * When a field is shown with logic, add the html and aria * required attributes if the field is required. */ function maybeAddRequiredTag( input ) { var isRequired, isOptional; if ( input.type === 'checkbox' || input.type === 'radio' || input.type === 'file' ) { return; } isRequired = input.parentElement.className.indexOf( 'frm_required_field' ); isOptional = input.className.indexOf( 'frm_optional' ); if ( isRequired > -1 && isOptional === -1 ) { input.setAttribute( 'aria-required', true ); } } /** * Don't loop through every input in a radio/checkbox/other field * TODO: Improve this for checkboxes and address fields */ function skipSetValue( i, prevInput, inputs ) { var typeArray = [ 'checkbox', 'radio' ]; if ( i < 1 || prevInput === undefined ) { return false; } if ( null !== inputs[ i ].getAttribute( 'data-frmprice' ) ) { // never skip calculating a product price. return false; } var isOther = inputs[ i ].className.includes('frm_other_input'); return isOther || ( prevInput.name == inputs[ i ].name && typeArray.includes(prevInput.type) ); } // Check if a field input inside of a section or embedded form is conditionally hidden function isChildInputConditionallyHidden( input, formId ) { var fieldDivPart = frmFrontForm.getFieldId( input, true ), fieldDivId = 'frm_field_' + fieldDivPart + '_container'; return isFieldConditionallyHidden( fieldDivId, formId ); } function showFieldContainer( containerId ) { var $container = jQuery( '#' + containerId ).show(); if ( $container.hasClass( 'frm_inside_container' ) && null === $container.find( 'select' ).val() ) { $container.find( 'select' ).val( '' ).trigger( 'change' ); } } /** * @param {Object} depFieldArgs * @param {string} depFieldArgs.containerId * @param {string} depFieldArgs.formId * @param onCurrentPage */ function hideFieldAndClearValue( depFieldArgs, onCurrentPage ) { if ( isFieldConditionallyHidden( depFieldArgs.containerId, depFieldArgs.formId ) ) { return; } addToHideFields( depFieldArgs.containerId, depFieldArgs.formId ); if ( depFieldArgs.fieldType === 'submit' ) { if ( onCurrentPage ) { hideOrDisableSubmitButton( depFieldArgs ); } return; } if ( onCurrentPage ) { hideFieldContainer( depFieldArgs.containerId ); clearInputsInFieldOnPage( depFieldArgs.containerId ); } else { clearInputsInFieldAcrossPage( depFieldArgs ); } } /** * Hide or disable given submit button as appropriate, based on submit Conditional Logic * * @param depFieldArgs */ function hideOrDisableSubmitButton( depFieldArgs ) { if ( depFieldArgs.containerId == undefined ) { depFieldArgs.containerId = getSubmitButtonContainerID( depFieldArgs ); } addSubmitButtonToHiddenList( depFieldArgs ); if ( depFieldArgs.hideDisable && depFieldArgs.hideDisable === 'disable' ) { disableButton( '#' + depFieldArgs.containerId ); } else { hideFieldContainer( depFieldArgs.containerId ); } } /** * Add submit button to list of hidden submit buttons on page * * @param depFieldArgs */ function addSubmitButtonToHiddenList( depFieldArgs ) { hiddenSubmitButtons.push( depFieldArgs.formKey ); } /** * Checks if a particular submit button on the current page is hidden * * @param {string} formKey * @returns {boolean} */ function isOnPageSubmitButtonHidden( formKey ) { return hiddenSubmitButtons.includes(formKey); } /** * Hide submit button with specified container ID that was previously hidden. * * @param submitContainerID */ function hidePreviouslyHiddenSubmitButton( submitContainerID ) { var formId = submitContainerID.replace( 'frm_form_', '' ); formId = formId.replace( '_container .frm_final_submit', '' ); var depFieldArgs = getRulesForSingleField( 'submit_' + formId ); if ( depFieldArgs ) { hideOrDisableSubmitButton( depFieldArgs ); } } /** * Get the form key (e.g. t3cq5) from the form element id (e.g. form_t3cq5) * * @param elementId * @returns {string} form key */ function getFormKeyFromFormElementID( elementId ) { return elementId.replace( 'form_', '' ); } function hideFieldContainer( containerId ) { jQuery( '#' + containerId ).hide(); } /** * Disable button with specified selector * * @param buttonSelector */ function disableButton( buttonSelector ) { jQuery( buttonSelector ).prop( 'disabled', true ); } function jsonParse( str ) { try { var obj = JSON.parse( str ); return obj; } catch ( e ) { return false; } } function clearInputsInFieldOnPage( containerId ) { var inputs = getInputsInFieldOnPage( containerId ); clearValueForInputs( inputs, 'required' ); } function clearInputsInFieldAcrossPage( depFieldArgs ) { var inputs = getInputsInFieldAcrossPage( depFieldArgs ); clearValueForInputs( inputs ); } /** * Get all the child inputs in a hidden section (across a page) * * @param {Object} depFieldArgs * @param {string} depFieldArgs.fieldType * @param {string} depFieldArgs.fieldId */ function getInputsInHiddenSection( depFieldArgs ) { // If a section, get all inputs with data attribute var inputs = []; if ( depFieldArgs.fieldType === 'divider' ) { inputs = document.querySelectorAll( '[data-sectionid="' + depFieldArgs.fieldId + '"]' ); } return inputs; } // Get all the child inputs in a hidden embedded form (across a page) function getInputsInHiddenEmbeddedForm( depFieldArgs ) { // TODO: maybe remove form and [0]? // TODO: what if someone is using embeddedformfieldkey-moretext as their field key? Add data attribute for this return document.querySelectorAll( '[id^="field_' + depFieldArgs.fieldKey + '-"]' ); } /** * Changes dropdown placeholder option color. * * @since 6.16.1 * * @param {HTMLElement} select * * @returns {Void} */ function changeSelectColor( select ) { if ( select.options[select.selectedIndex]?.classList.contains( 'frm-select-placeholder' ) ) { const styleElement = select.closest( '.with_frm_style' ); const textColorDisabled = styleElement ? getComputedStyle( styleElement ).getPropertyValue( '--text-color-disabled' ).trim() : ''; select.style.setProperty( 'color', textColorDisabled, 'important' ); } } /** * Clears value of field inputs. * * @since 5.4 Added the third param. * * @param {Array} inputs Array of inputs. * @param {String} required Required string. * @param {Boolean} resetToDefault Is `true` if reset to default value. Otherwise, reset to empty value. */ function clearValueForInputs( inputs, required, resetToDefault ) { var prevInput, blankSelect, valueChanged, l, i, input, defaultVal, reset, linkedRadioInput; if ( inputs.length < 1 ) { return; } valueChanged = true; l = inputs.length; for ( i = 0; i < l; i++ ) { input = inputs[ i ]; defaultVal = input.getAttribute( 'data-frmval' ); reset = resetToDefault && defaultVal; // Don't remove values from some fields. if ( input.className.includes('frm_dnc') || input.name.includes('[row_ids]') ) { prevInput = input; continue; } if ( i > 0 && prevInput.name != input.name && valueChanged === true ) { // Only trigger a change after all inputs in a field are cleared triggerChange( jQuery( prevInput ) ); } valueChanged = true; if ( input.type === 'radio' || input.type === 'checkbox' ) { if ( ! reset ) { input.checked = false; } else if ( 'radio' === input.type ) { input.checked = defaultVal === input.value; } else { resetCheckboxInputToValue( input, defaultVal ); } maybeClearStarRatingInput( input ); } else if ( input.tagName === 'SELECT' ) { if ( isSlimSelect( input ) ) { setSlimValue( input, reset ? defaultVal : '' ); } else { if ( ! reset && resetToDefault && input.getAttribute( 'data-placeholder' ) ) { // When a dropdown has no default value, a placeholder option is added. // We want to reset to that, as it works the same as an empty default value. reset = true; defaultVal = ''; } if ( ! reset ) { blankSelect = input.selectedIndex === 0 && input.options[ 0 ].text.trim() === ''; if ( blankSelect || ( input.selectedIndex === -1 ) ) { valueChanged = false; } else { input.selectedIndex = -1; } } else { valueChanged = resetSelectInputToValue( input, defaultVal ); } const chosenId = input.id.replace( /[^\w]/g, '_' ); // Match what the script is doing. const autocomplete = document.getElementById( chosenId + '_chosen' ); if ( autocomplete !== null ) { jQuery( input ).trigger( 'chosen:updated' ); } } } else if ( input.type === 'range' ) { let sliderDefault = defaultVal; if ( ! reset ) { // Reset to mid value const min = parseFloat( input.getAttribute( 'min' ) ); const max = parseFloat( input.getAttribute( 'max' ) ); const mid = ( ( max - min ) / 2 ) + min; const step = parseFloat( input.getAttribute( 'step' ), 10 ); const midSteps = Math.round( mid / step ); sliderDefault = midSteps * step; input.parentElement.querySelector( '.frm_range_value' ).textContent = sliderDefault; } input.value = sliderDefault; initRangeInput( [ input ] ); } else if ( input.getAttribute( 'data-frmprice' ) !== null ) { setHiddenProduct( input ); } else { if ( ! reset ) { input.value = ''; } else { input.value = defaultVal; } linkedRadioInput = input.id.includes('-otext') ? document.getElementById( input.id.replace( '-otext', '' ) ) : null; if ( linkedRadioInput && linkedRadioInput.checked === false ) { input.classList.add( 'frm_pos_none' ); } if ( null !== input.getAttribute( 'data-frmfile' ) ) { clearDropzoneFiles( input ); } if ( input.type === 'hidden' && input.closest( '.frm_range_container ' ) ) { resetRangeInput( input ); } } if ( required === 'required' ) { input.required = false; input.setAttribute( 'aria-required', false ); } prevInput = inputs[ i ]; } // trigger a change for the final input in the loop if ( valueChanged === true ) { triggerChange( jQuery( prevInput ) ); } } function resetRangeInput( input ) { const rangeContainer = input.parentElement; const rangeSliderWrapperClone = rangeContainer.querySelector( '.frm-slider-wrapper' ).cloneNode( true ); // Replace old range slider elements with cloned ones to prevent jerky behavior after reset when initializeRangeSlider is called. rangeContainer.querySelector( 'div:last-of-type' ).replaceChild( rangeSliderWrapperClone, rangeContainer.querySelector( '.frm-slider-wrapper' ) ); initializeRangeSlider( input, rangeContainer ); } function setSlimValue( input, value ) { if ( value.length && '[' === value[0] && -1 !== value.indexOf( ',' ) && ']' === value[ value.length - 1 ] && 'multiple' === input.getAttribute( 'multiple' ) ) { // Array values are stored as a JSON string. value = JSON.parse( value ); } input.slim.setSelected( value ); } /** * Maybe clear star rating. * * @since 5.4 * * @param {HTMLElement} input This should be the last input in star rating. */ function maybeClearStarRatingInput( input ) { var starGroup, checkedInput; if ( 'radio' !== input.type || ! input.matches( '.frm-star-group input:last-of-type' ) ) { return; } starGroup = input.closest( '.frm-star-group' ); checkedInput = starGroup.querySelector( 'input:checked' ); if ( checkedInput ) { updateStars( checkedInput ); } else { clearStars( starGroup, true ); } } /** * Resets checkbox input to a value. * * @since 5.4 * * @param {HTMLElement} input Checkbox element. * @param {Object} val The value. */ function resetCheckboxInputToValue( input, val ) { var i; val = jsonParse( val ); if ( ! val ) { return; } for ( i in val ) { if ( val[ i ] === input.value ) { input.checked = true; return; } } input.checked = false; } /** * Resets select input to a value. * * @since 5.4 * * @param {HTMLElement} input Select element. * @param {String|Object} val The value. Is object if multiselect. * @return {Boolean} Return `true` if value is changed. */ function resetSelectInputToValue( input, val ) { if ( input.multiple ) { return resetMultiSelectInputToValue( input, val ); } var i, valueChanged = false, options = input.querySelectorAll( 'option' ); for ( i = 0; i < options.length; i++ ) { if ( val === options[ i ].value && ! options[ i ].selected ) { options[ i ].selected = true; valueChanged = true; continue; } if ( val !== options[ i ].value && options[ i ].selected ) { options[ i ].selected = false; valueChanged = true; } } return valueChanged; } /** * Resets multiselect input to a value. * * @since 5.4 * * @param {HTMLElement} input Select element. * @param {Object} val The value. * @return {Boolean} Return `true` if value is changed. */ function resetMultiSelectInputToValue( input, val ) { val = jsonParse( val ); if ( ! val ) { return false; } var i, contained, valueChanged = false, options = input.querySelectorAll( 'option' ); for ( i = 0; i < options.length; i++ ) { contained = objectContainValue( val, options[ i ].value ); if ( contained && ! options[ i ].selected ) { options[ i ].selected = true; valueChanged = true; continue; } if ( ! contained && options[ i ].selected ) { options[ i ].selected = false; valueChanged = true; } } return valueChanged; } /** * Checks if object contains a value. * * @since 5.4 * * @param {Object} obj Object. * @param {Mixed} val Value. * @return {Boolean} */ function objectContainValue( obj, val ) { var x; for ( x in obj ) { if ( obj[ x ] === val ) { return true; } } return false; } function clearDropzoneFiles( hiddenFileIdField ) { var dropzoneElement = hiddenFileIdField.nextElementSibling; if ( dropzoneElement && -1 !== dropzoneElement.className.indexOf( 'frm_dropzone' ) && 'object' === typeof dropzoneElement.dropzone && 'function' === typeof dropzoneElement.dropzone.removeAllFiles ) { dropzoneElement.dropzone.removeAllFiles( true ); } } function isFieldCurrentlyShown( containerId, formId ) { return isFieldConditionallyHidden( containerId, formId ) === false; } function isFieldConditionallyHidden( containerId, formId ) { var hidden = false, hiddenFields = getHiddenFields( formId ); if ( hiddenFields.includes(containerId) ) { hidden = true; } return hidden; } function clearHideFields() { var hideFieldInputs = document.querySelectorAll( '[id^="frm_hide_fields_"]' ); clearValueForInputs( hideFieldInputs ); } function addToHideFields( htmlFieldId, formId ) { //TODO: why is this run on submit? // Get all currently hidden fields var hiddenFields = getHiddenFields( formId ); if ( hiddenFields.includes(htmlFieldId) ) { // If field id is already in the array, move on } else { // Add new conditionally hidden field to array hiddenFields.push( htmlFieldId ); // Set the hiddenFields value in the frm_hide_field_formID input hiddenFields = JSON.stringify( hiddenFields ); var frmHideFieldsInput = document.getElementById( 'frm_hide_fields_' + formId ); if ( frmHideFieldsInput !== null ) { frmHideFieldsInput.value = hiddenFields; } } } function getAllHiddenFields() { var formId, i, hiddenFields = [], hideFieldInputs = document.querySelectorAll( '*[id^="frm_hide_fields_"]' ), formTotal = hideFieldInputs.length; for ( i = 0; i < formTotal; i++ ) { formId = hideFieldInputs[ i ].id.replace( 'frm_hide_fields_', '' ); hiddenFields = hiddenFields.concat( getHiddenFields( formId ) ); } return hiddenFields; } function getHiddenFields( formId ) { var hiddenFields = []; // Fetch the hidden fields from the frm_hide_fields_formId input var frmHideFieldsInput = document.getElementById( 'frm_hide_fields_' + formId ); if ( frmHideFieldsInput === null ) { return hiddenFields; } hiddenFields = frmHideFieldsInput.value; if ( hiddenFields ) { hiddenFields = JSON.parse( hiddenFields ); } else { hiddenFields = []; } return hiddenFields; } function setDefaultValue( input, inContainer ) { var placeholder, isMultipleSelect, $input = jQuery( input ), defaultValue = $input.data( 'frmval' ); if ( defaultValue === undefined && input.classList.contains( 'wp-editor-area' ) ) { // set value in rich text var defaultField = document.getElementById( input.id + '-frmval' ); if ( defaultField !== null ) { defaultValue = defaultField.value; var targetTinyMceEditor = tinymce.get( input.id ); if ( null !== targetTinyMceEditor ) { targetTinyMceEditor.setContent( defaultValue ); } } } else if ( defaultValue === undefined && input.type === 'hidden' ) { //with read only select input, value is in sibling select var $select = $input.next( 'select[disabled]' ); if ( $select.length > 0 ) { defaultValue = $select.data( 'frmval' ); } } placeholder = defaultValue; defaultValue = setDropdownPlaceholder( defaultValue, input ); if ( placeholder !== defaultValue ) { placeholder = true; } if ( defaultValue !== undefined ) { var numericKey = new RegExp( /\[\d*\]$/i ); if ( input.type === 'checkbox' || input.type === 'radio' ) { setCheckboxOrRadioDefaultValue( input.name, defaultValue ); } else if ( input.type === 'hidden' && input.name.includes('[]') ) { setHiddenCheckboxDefaultValue( input.name, defaultValue ); // Set for hidden checkbox fields that aren't on the current page. Skip hidden fields in a repeater. } else if ( ! inContainer && input.type === 'hidden' && input.name.includes('][') && numericKey.test( input.name ) ) { setHiddenCheckboxDefaultValue( input.name.replace( numericKey, '' ), defaultValue ); } else { isMultipleSelect = false; if ( placeholder && 'boolean' !== typeof placeholder && input.tagName === 'SELECT' && -1 !== input.className.indexOf( 'frm_chzn' ) ) { placeholder = false; } if ( 'SELECT' === input.tagName && 'multiple' === input.getAttribute( 'multiple' ) ) { isMultipleSelect = true; } if ( defaultValue.constructor === Object ) { if ( ! isMultipleSelect ) { var addressType = input.getAttribute( 'name' ).split( '[' ).slice( -1 )[ 0 ]; if ( addressType !== null ) { addressType = addressType.replace( ']', '' ); defaultValue = defaultValue[ addressType ]; if ( defaultValue === undefined ) { defaultValue = ''; } } } } if ( isMultipleSelect ) { selectMultiselectOptions( input, defaultValue ); } else { if ( typeof defaultValue === 'object' ) { // Don't replace a field shortcode with the id. defaultValue = '[' + defaultValue + ']'; } if ( isSlimSelect( input ) ) { // This is true even when no placeholder or default is set when autocomplete is enabled. // So we need to check if the default value is an empty string and remove the hidden placeholder. if ( '' === defaultValue ) { maybeRemoveHiddenPlaceholder( input ); } // Convert numbers to strings so SlimSelect matches properly. input.slim.setSelected( `${ defaultValue }` ); } else if ( input.classList.contains( 'frm_chzn' ) ) { if ( '' === defaultValue ) { maybeRemoveHiddenPlaceholder( input ); } jQuery( input ).val( defaultValue ).trigger( 'chosen:updated' ); } else { input.value = defaultValue; // Trigger change event for range slider input so it knows to sync the handles. if ( 'hidden' === input.type && input.dataset.isRangeSliderInitialized ) { input.dispatchEvent( new Event( 'change' ) ); } if ( 'range' === input.type ) { // Trigger input event input.dispatchEvent( new Event( 'input' ) ); } } } if ( 'SELECT' === input.tagName ) { changeSelectColor( input ); } } if ( ! placeholder && input.tagName === 'SELECT' ) { maybeUpdateChosenOptions( input ); if ( input.value === '' ) { setOtherSelectValue( input, defaultValue ); } } triggerChange( $input ); } else if ( 'SELECT' === input.tagName ) { maybeRemoveHiddenPlaceholder( input ); } } /** * @since 6.16.2 * * @param {HTMLElement} input A select element. * @return {boolean} True if the hidden placeholder is detected and removed. */ function maybeRemoveHiddenPlaceholder( input ) { const hiddenPlaceholder = input.querySelector( 'option[value=""].frm_hidden.frm_hidden_placeholder' ); if ( ! hiddenPlaceholder ) { return false; } hiddenPlaceholder.remove(); return true; } function isSlimSelect( input ) { return input.classList.contains( 'frm_slimselect' ) && 'object' === typeof input.slim; } function selectMultiselectOptions( select, values ) { if ( isSlimSelect( select ) && 'function' === typeof Object.values ) { select.slim.setSelected( Object.values( values ) ); return; } var valueKey, option; for ( valueKey in values ) { option = select.querySelector( 'option[value="' + values[ valueKey ] + '"]' ); if ( option ) { option.selected = true; } } } /** * Select the option with placeholder if applicable. */ function setDropdownPlaceholder( defaultValue, input ) { var placeholder; if ( defaultValue === undefined && input.tagName === 'SELECT' ) { placeholder = input.getAttribute( 'data-placeholder' ); if ( placeholder !== null ) { defaultValue = ''; } } return defaultValue; } function setCheckboxOrRadioDefaultValue( inputName, defaultValue ) { // Get all checkbox/radio inputs for this field var radioInputs = document.getElementsByName( inputName ), isSet = false, firstInput = false; if ( typeof defaultValue === 'object' ) { // Convert the object to an array. defaultValue = Object.keys( defaultValue ).map( ( key ) => { return defaultValue[ key ]; } ); } let hiddenFieldIndex = 0; // Loop through options and set the default value for ( var i = 0, l = radioInputs.length; i < l; i++ ) { if ( firstInput === false ) { firstInput = radioInputs[ i ]; } if ( radioInputs[ i ].type === 'hidden' ) { // If field is read-only and there is a hidden input if ( Array.isArray( defaultValue ) && defaultValue[ hiddenFieldIndex ] !== null && 'undefined' !== typeof defaultValue[ hiddenFieldIndex ] ) { radioInputs[ i ].value = defaultValue[ hiddenFieldIndex ]; } else { radioInputs[ i ].value = defaultValue; } hiddenFieldIndex++; isSet = true; } else if ( radioInputs[ i ].value == defaultValue || ( Array.isArray( defaultValue ) && defaultValue.includes(radioInputs[ i ].value) ) ) { // If input's value matches the default value, set checked to true radioInputs[ i ].checked = true; isSet = true; if ( radioInputs[ i ].type === 'radio' ) { break; } } } if ( ! isSet && firstInput !== false ) { setOtherValueLimited( firstInput, defaultValue ); } } // Set the default value for hidden checkbox or multi-select dropdown fields function setHiddenCheckboxDefaultValue( inputName, defaultValue ) { // Get all the hidden inputs with the same name var hiddenInputs = jQuery( 'input[name^="' + inputName + '"]' ).get(); if ( typeof defaultValue === 'object' ) { // Convert the object to an array. defaultValue = Object.keys( defaultValue ).map( ( key ) => { return defaultValue[ key ]; } ); } if ( Array.isArray( defaultValue ) ) { for ( var i = 0, l = defaultValue.length; i < l; i++ ) { if ( i in hiddenInputs ) { hiddenInputs[ i ].value = defaultValue[ i ]; } else { // TODO: accommodate for when there are multiple default values but the user has removed some } } } else if ( hiddenInputs[ 0 ] !== null && hiddenInputs[ 0 ] !== undefined ) { hiddenInputs[ 0 ].value = defaultValue; } } function removeFromHideFields( htmlFieldId, formId ) { // Get all currently hidden fields var hiddenFields = getHiddenFields( formId ); // If field id is in the array, delete it var itemIndex = hiddenFields.indexOf( htmlFieldId ); if ( itemIndex > -1 ) { // Remove field from the hiddenFields array hiddenFields.splice( itemIndex, 1 ); // Update frm_hide_fields_formId input hiddenFields = JSON.stringify( hiddenFields ); var frmHideFieldsInput = document.getElementById( 'frm_hide_fields_' + formId ); frmHideFieldsInput.value = hiddenFields; } } /***************************************************** * Lookup Field Functions ******************************************************/ /** * Check all fields that are "watching" a lookup field that changed */ function checkFieldsWatchingLookup( fieldId, changedInput, originalEvent ) { if ( typeof __FRMLOOKUP === 'undefined' || __FRMLOOKUP[ fieldId ] === undefined || __FRMLOOKUP[ fieldId ].dependents.length < 1 || changedInput === null || changedInput === undefined ) { return; } var triggerFieldArgs = __FRMLOOKUP[ fieldId ]; var parentRepeatArgs = getRepeatArgsFromFieldName( changedInput[ 0 ].name ); for ( var i = 0, l = triggerFieldArgs.dependents.length; i < l; i++ ) { updateWatchingFieldById( triggerFieldArgs.dependents[ i ], parentRepeatArgs, originalEvent ); } } /** * Update all instances of a "watching" field * * @since 2.01.0 * * @param {string} fieldId * @param {Object} parentRepeatArgs * @param {string} originalEvent * @return {void} */ function updateWatchingFieldById( fieldId, parentRepeatArgs, originalEvent ) { var childFieldArgs = getLookupArgsForSingleField( fieldId ); // If lookup field has no parents, no need to update this field if ( childFieldArgs === false || childFieldArgs.parents.length < 1 ) { return; } if ( childFieldArgs.fieldType === 'lookup' ) { updateLookupFieldOptions( childFieldArgs, parentRepeatArgs ); } else if ( originalEvent === 'value changed' ) { // If the original event was NOT triggered from a direct value change to the Lookup field, // do not update the text field value. updateWatchingFieldValue( childFieldArgs, parentRepeatArgs ); } } /** * Update a Lookup field's options * * @param {Object} childFieldArgs * @param {Object} parentRepeatArgs * @param {String} parentRepeatArgs.repeatRow */ function updateLookupFieldOptions( childFieldArgs, parentRepeatArgs ) { var childFieldElements = []; if ( parentRepeatArgs.repeatRow !== '' ) { childFieldElements = getRepeatingFieldDivOnCurrentPage( childFieldArgs, parentRepeatArgs ); } else { childFieldElements = getAllFieldDivsOnCurrentPage( childFieldArgs ); } for ( var i = 0, l = childFieldElements.length; i < l; i++ ) { addRepeatRow( childFieldArgs, childFieldElements[ i ].id ); updateSingleLookupField( childFieldArgs, childFieldElements[ i ]); } processPendingLookups(); } /** * Go through the list of pending look ups and process them all. * These are done in batches of 20 at a time. * * @since 6.15 * * @return {void} */ function processPendingLookups() { const unprocessedPendingLookups = getUnprocessedPendingLookups(); if ( ! unprocessedPendingLookups.length ) { return; } const formId = unprocessedPendingLookups[0].childFieldArgs.formId; // We want to allow duplicate form ids (inside repeaters) here because the number of calls // to enableFormAfterLookup matters for handling the processesRunning variable. const allFormIds = []; const batchSize = 20; const batches = Math.ceil( unprocessedPendingLookups.length / batchSize ); for ( let batchNumber = 0; batchNumber < batches; ++batchNumber ) { const start = batchNumber * batchSize; const end = start + batchSize; const currentBatch = unprocessedPendingLookups.slice( start, end ); currentBatch.forEach( ( pendingLookup ) => { pendingLookup.pending = false; allFormIds.push( pendingLookup.childFieldArgs.formId ); } ); postToAjaxUrl( getFormById( formId ), { action: 'frm_replace_lookup_field_options_arr', postData: currentBatch.map( pendingLookup => pendingLookup.childFieldArgs ), nonce: frm_js.nonce }, ( newOptionsByFieldId ) => { allFormIds.forEach( enableFormAfterLookup ); Object.entries( newOptionsByFieldId ).forEach( entry => { const key = entry[0]; const newOptions = entry[1]; const optionLabels = 'undefined' !== typeof newOptionsByFieldId[ key + '_label' ] ? newOptionsByFieldId[ key + '_label' ] : []; currentBatch.forEach( function( pendingLookup ) { if ( ! isNaN( key ) && pendingLookup.childFieldArgs.unique === parseInt( key ) ) { pendingLookup.callback( newOptions, optionLabels ); } } ); }); }, false, { dataType: 'json' } ); } } /** * @return {Array} */ function getUnprocessedPendingLookups() { return pendingLookupFieldAjax.filter( pendingLookup => pendingLookup.pending ); } /** * Get the div for a repeating field on the current page * @param {Object} childFieldArgs * @param {string} childFieldArgs.fieldId * @param {Object} parentRepeatArgs * @param {string} parentRepeatArgs.repeatingSection * @param {string} parentRepeatArgs.repeatRow * @returns {Array} */ function getRepeatingFieldDivOnCurrentPage( childFieldArgs, parentRepeatArgs ) { var childFieldDivs = [], selector = 'frm_field_' + childFieldArgs.fieldId + '-'; selector += parentRepeatArgs.repeatingSection + '-' + parentRepeatArgs.repeatRow + '_container'; var container = document.getElementById( selector ); if ( container !== null ) { childFieldDivs.push( container ); } return childFieldDivs; } function updateWatchingFieldValue( childFieldArgs, parentRepeatArgs ) { var childFieldElements = getAllTextFieldInputs( childFieldArgs, parentRepeatArgs ); for ( var i = 0, l = childFieldElements.length; i < l; i++ ) { addRepeatRowForInput( childFieldElements[ i ].name, childFieldArgs ); updateSingleWatchingField( childFieldArgs, childFieldElements[ i ]); } } /** * Get the Lookup Args for a field ID * * @param {string} fieldId * @return {boolean|Object} */ function getLookupArgsForSingleField( fieldId ) { if ( typeof __FRMLOOKUP === 'undefined' || __FRMLOOKUP[ fieldId ] === undefined ) { return false; } return __FRMLOOKUP[ fieldId ]; } /** * Update a single Lookup field * * @since 2.01.0 * @param {Object} childFieldArgs * @param {string} childFieldArgs.inputType * @param {object} childElement */ function updateSingleLookupField( childFieldArgs, childElement ) { childFieldArgs.parentVals = getParentLookupFieldVals( childFieldArgs ); if ( childFieldArgs.inputType === 'select' ) { maybeReplaceSelectLookupFieldOptions( childFieldArgs, childElement ); } else if ( childFieldArgs.inputType === 'radio' || childFieldArgs.inputType === 'checkbox' ) { maybeReplaceCbRadioLookupOptions( childFieldArgs, childElement ); } else if ( childFieldArgs.inputType === 'data' ) { maybeReplaceLookupList( childFieldArgs, childElement ); } } /** * Update a standard field that is "watching" a Lookup * * @since 2.01.0 * * @param {Object} childFieldArgs * @param {object} childElement * @return {void} */ function updateSingleWatchingField( childFieldArgs, childElement ) { childFieldArgs.parentVals = getParentLookupFieldVals( childFieldArgs ); if ( currentLookupHasQueue( childElement.id ) ) { addLookupToQueueOfTwo( childFieldArgs, childElement ); return; } addLookupToQueueOfTwo( childFieldArgs, childElement ); maybeInsertValueInFieldWatchingLookup( childFieldArgs, childElement ); } /** * Get all the occurrences of a specific Text field * * @since 2.01.0 * @param {Object} childFieldArgs * @param {boolean} childFieldArgs.isRepeating * @param {string} childFieldArgs.fieldKey * @param {Object} parentRepeatArgs * @param {string} parentRepeatArgs.repeatingSection * @param {string} parentRepeatArgs.repeatRow * @return {NodeList} */ function getAllTextFieldInputs( childFieldArgs, parentRepeatArgs ) { var selector = 'field_' + childFieldArgs.fieldKey; if ( childFieldArgs.isRepeating ) { if ( parentRepeatArgs.repeatingSection !== '' ) { // If trigger field is repeating/embedded, use its section row in selector selector = '[id="' + selector + '-' + parentRepeatArgs.repeatRow + '"]'; } else { // If trigger field is not repeating/embedded, get all repeating field inputs selector = '[id^="' + selector + '-"]'; } } else { selector = '[id="' + selector + '"]'; } return document.querySelectorAll( selector ); } // Set the value in a regular field that is watching a lookup field when it is conditionally shown function maybeSetWatchingFieldValue( input ) { var fieldId = frmFrontForm.getFieldId( input, false ), childFieldArgs = getLookupArgsForSingleField( fieldId ); // If lookup field has no parents, no need to update this field if ( childFieldArgs === false || childFieldArgs.fieldType === 'lookup' ) { return; } updateSingleWatchingField( childFieldArgs, input ); } /** * Get all divs on the current page for a given field * * @since 2.01.0 * @param {Object} childFieldArgs * @param {boolean} childFieldArgs.isRepeating * @param {string} childFieldArgs.fieldId * @returns {Array} */ function getAllFieldDivsOnCurrentPage( childFieldArgs ) { var childFieldDivs = []; if ( childFieldArgs.isRepeating ) { childFieldDivs = document.querySelectorAll( '.frm_field_' + childFieldArgs.fieldId + '_container' ); } else { var container = document.getElementById( 'frm_field_' + childFieldArgs.fieldId + '_container' ); if ( container !== null ) { childFieldDivs.push( container ); } } return childFieldDivs; } // Get the field values from all parents function getParentLookupFieldVals( childFieldArgs ) { var parentFieldArgs, parentVals = [], parentIds = childFieldArgs.parents, parentValue = false; for ( var i = 0, l = parentIds.length; i < l; i++ ) { parentFieldArgs = getLookupArgsForSingleField( parentIds[ i ]); parentValue = getFieldValue( parentFieldArgs, childFieldArgs ); // If any parents have blank values, don't waste time looking for values if ( parentValue === '' || parentValue === false ) { parentVals = false; break; } parentVals[ i ] = parentValue; } return parentVals; } // Get the value from array of radio inputs (could be type="hidden" or type="radio") function getValueFromRadioInputs( radioInputs ) { var radioValue = false, l = radioInputs.length; for ( var i = 0; i < l; i++ ) { if ( radioInputs[ i ].type === 'hidden' || radioInputs[ i ].checked ) { radioValue = radioInputs[ i ].value; break; } } return radioValue; } /** * Maybe replace the options in a Select Lookup field * * @since 2.01.0 * @param {Object} childFieldArgs * @param {Array} childFieldArgs.parents * @param {Array} childFieldArgs.parentVals * @param {string} childFieldArgs.fieldId * @param {string} childFieldArgs.fieldKey * @param {string} childFieldArgs.formId * @param {object} childDiv */ function maybeReplaceSelectLookupFieldOptions( childFieldArgs, childDiv ) { // Get select within childDiv var childSelect = childDiv.getElementsByTagName( 'SELECT' )[ 0 ]; if ( childSelect === null ) { return; } var currentValue = childSelect.value; if ( childFieldArgs.parentVals === false ) { // If any parents have blank values, don't waste time looking for values childSelect.options.length = 1; childSelect.value = ''; maybeUpdateChosenOptions( childSelect ); if ( currentValue !== '' ) { triggerChange( jQuery( childSelect ), childFieldArgs.fieldKey ); } } else { disableLookup( childSelect ); disableFormPreLookup( childFieldArgs.formId ); getLookupValues( childFieldArgs, ( newOptions, optionLabels ) => { replaceSelectLookupFieldOptions( childFieldArgs, childSelect, newOptions, optionLabels ); triggerLookupOptionsLoaded( jQuery( childDiv ) ); enableFormAfterLookup( childFieldArgs.formId ); } ); } } // Update chosen options if autocomplete is enabled function maybeUpdateChosenOptions( childSelect ) { if ( childSelect.className.includes('frm_chzn') && jQuery().chosen ) { jQuery( childSelect ).trigger( 'chosen:updated' ); } } /** * Disable a Select Lookup field and add loading image * * @since 2.02.11 * @param {object} childSelect */ function disableLookup( childSelect ) { childSelect.className = childSelect.className + ' frm_loading_lookup'; childSelect.disabled = true; maybeUpdateChosenOptions( childSelect ); } /** * Disable a form prior to a Lookup field's Ajax request * * @since 2.03.02 * @param {String} formId */ function disableFormPreLookup( formId ) { processesRunning++; if ( processesRunning === 1 ) { var form = getFormById( formId ); if ( form !== null ) { frmFrontForm.showSubmitLoading( jQuery( form ) ); } } } /** * Enable a form if all Lookup field requests are completed * * @since 2.03.02 * @param {String} formId */ function enableFormAfterLookup( formId ) { var form; processesRunning--; if ( processesRunning <= 0 ) { form = getFormById( formId ); if ( form !== null ) { removeSubmitLoading( form, formId, processesRunning ); } } } /** * Get a form element by the ID number * * @since 2.03.02 * @param {string} formId * @returns {Element} */ function getFormById( formId ) { var form = document.querySelector( '#frm_form_' + formId + '_container form' ); if ( form === null ) { // The order is switched on the back end. form = document.getElementById( 'frm_form_' + formId + '_container' ); if ( form !== null ) { form = form.closest( 'form' ); } } return form; } /** * Enable a Select Lookup field and remove loading image * * @since 2.02.11 * @param {object} childSelect * @param {boolean} isReadOnly */ function enableLookup( childSelect, isReadOnly ) { if ( isReadOnly === false ) { childSelect.disabled = false; } childSelect.className = childSelect.className.replace( ' frm_loading_lookup', '' ); } /** * @param {HTMLElement} element * @returns {bool} */ function isMultiSelect( element ) { return element.tagName.toLowerCase() === 'select' && element.multiple; } /** * Gets selected options for a multiselect element. * * @param {HTMLElement} element * @returns {Array} */ function getSelectedOptions( element ) { if ( isSlimSelect( element ) ) { return element.slim.getSelected(); } if ( ! isMultiSelect( element ) ) { return element.value; } var i, option, selectedOptions = []; for ( i = 0; i < element.options.length; i++ ) { option = element.options[i]; if ( option.selected ) { selectedOptions.push( option.value ); } } return selectedOptions; } /** * Sets selected options for a dropdown element. * * @param {HTMLElement} element * @param {Array} values */ function setSelectedOptions( element, values ) { var option; if ( ! isMultiSelect( element ) ) { element.value = values; return; } // Reset all options to unselected Array.from( element.options ).forEach( ( option ) => { option.selected = false; }); // Set selected attribute for the specified values values.forEach( ( value ) => { option = element.querySelector( 'option[value="' + value + '"]' ); if ( option ) { option.selected = true; } }); } /** * Replace the options in a Select Lookup field * * @since 2.01.0 * @param {Object} fieldArgs * @param {string} fieldArgs.fieldKey * @param {boolean} fieldArgs.isReadOnly * @param {object} childSelect * @param {Array} newOptions */ function replaceSelectLookupFieldOptions( fieldArgs, childSelect, newOptions, optionLabels ) { var origVal, i, optsLength, newOption, optionData; // Get selected options origVal = getSelectedOptions( childSelect ); // Remove old options for ( i = childSelect.options.length; i > 0; i-- ) { childSelect.remove( i ); } // Add new options optsLength = newOptions.length; optionData = []; if ( childSelect.options.length > 0 && '' === childSelect.options[0].value ) { optionData.push({ text: childSelect.options[0].textContent, value: childSelect.options[0].value, placeholder: true }); } for ( i = 0; i < optsLength; i++ ) { const optionLabel = optionLabels[ i ] || newOptions[ i ]; newOption = new Option( optionLabel, newOptions[ i ], false, false ); childSelect.options[ i + 1 ] = newOption; optionData.push( newOption ); } if ( childSelect.slim ) { childSelect.slim.setData( optionData ); } setSelectLookupVal( childSelect, origVal ); enableLookup( childSelect, fieldArgs.isReadOnly ); maybeUpdateChosenOptions( childSelect ); // Trigger a change if the new value is different from the old value if ( getSelectedOptions( childSelect ).toString() !== origVal.toString() ) { triggerChange( jQuery( childSelect ), fieldArgs.fieldKey ); } } /** * Set the value in a refreshed Lookup Field. * * @param {HTMLElement} childSelect * @param {Array|string} origVal * @returns {void} */ function setSelectLookupVal( childSelect, origVal ) { if ( isSlimSelect( childSelect ) ) { if ( ! valueIsSingleItemArrayWithEmptyString( origVal ) ) { setSlimValue( childSelect, origVal ); } return; } // Try setting the dropdown to the original value setSelectedOptions( childSelect, origVal ); if ( childSelect.value === '' ) { // If the original value is no longer present, try setting to default value var defaultValue = childSelect.getAttribute( 'data-frmval' ); if ( defaultValue !== null ) { childSelect.value = defaultValue; } } if ( childSelect.value === '' ) { // Set the value to empty string again to fix the issue with blank option show. childSelect.value = ''; } } /** * @since 6.8.3 * * @param {Array|string} value * @returns {boolean} */ function valueIsSingleItemArrayWithEmptyString( value ) { return Array.isArray( value ) && 1 === value.length && '' === value[0]; } /** * Either hide checkbox/radio Lookup field or update its options * * @since 2.01.01 * @param {Object} childFieldArgs * @param {Array} childFieldArgs.parentVals * @param {object} childDiv */ function maybeReplaceCbRadioLookupOptions( childFieldArgs, childDiv ) { if ( childFieldArgs.parentVals === false ) { // If any parents have blank values, don't waste time looking for values var inputs = childDiv.getElementsByTagName( 'input' ); maybeHideRadioLookup( childFieldArgs, childDiv ); clearValueForInputs( inputs ); } else { replaceCbRadioLookupOptions( childFieldArgs, childDiv ); } } /** * Update the options in a checkbox/radio lookup field * * @since 2.01.01 * @param {Object} childFieldArgs * @param {string} childFieldArgs.inputType * @param {Array} childFieldArgs.parents * @param {Array} childFieldArgs.parentVals * @param {string} childFieldArgs.fieldId * @param {string} childFieldArgs.repeatRow * @param {string} childFieldArgs.fieldKey * @param {string} childFieldArgs.formId * @param {object} childDiv */ function replaceCbRadioLookupOptions( childFieldArgs, childDiv ) { var optContainer, inputs, currentValue, defaultValue, form, data, success; optContainer = childDiv.getElementsByClassName( 'frm_opt_container' )[ 0 ]; inputs = optContainer.getElementsByTagName( 'input' ); currentValue = ''; addLoadingIconJS( childDiv, optContainer ); if ( childFieldArgs.inputType == 'radio' ) { currentValue = getValueFromRadioInputs( inputs ); } else { currentValue = getValuesFromCheckboxInputs( inputs ); } defaultValue = jQuery( inputs[ 0 ]).data( 'frmval' ); disableFormPreLookup( childFieldArgs.formId ); form = getFormById( childFieldArgs.formId ); data = { action: 'frm_replace_cb_radio_lookup_options', parent_fields: childFieldArgs.parents, parent_vals: childFieldArgs.parentVals, field_id: childFieldArgs.fieldId, container_field_id: getContainerFieldId( childFieldArgs ), row_index: childFieldArgs.repeatRow, current_value: currentValue, default_value: defaultValue, nonce: frm_js.nonce }; success = function( newHtml ) { var input; optContainer.innerHTML = newHtml; removeLoadingIconJS( childDiv, optContainer ); if ( inputs.length == 1 && inputs[ 0 ].value === '' ) { maybeHideRadioLookup( childFieldArgs, childDiv ); } else { maybeShowRadioLookup( childFieldArgs, childDiv ); maybeSetDefaultCbRadioValue( childFieldArgs, inputs, defaultValue ); } input = inputs[0]; triggerChange( jQuery( input ), childFieldArgs.fieldKey ); triggerLookupOptionsLoaded( jQuery( childDiv ) ); enableFormAfterLookup( childFieldArgs.formId ); }; postToAjaxUrl( form, data, success ); } /** * @since 4.0.03 */ function maybeReplaceLookupList( childFieldArgs, childDiv ) { var inputs = childDiv.getElementsByTagName( 'input' ), content = inputs[0].previousElementSibling; if ( childFieldArgs.parentVals === false ) { // If any parents have blank values, don't waste time looking for values maybeHideRadioLookup( childFieldArgs, childDiv ); if ( content !== undefined ) { content.innerHTML = ''; } } else { getLookupValues( childFieldArgs, ( response, optionLabels ) => { content.innerHTML = optionLabels.length ? optionLabels.join( ', ' ) : response.join( ', ' ); inputs[0].value = response; maybeShowRadioLookup( childFieldArgs, childDiv ); triggerLookupOptionsLoaded( jQuery( childDiv ) ); }); } } /** * Get the lookup options in an array. * @since 4.0.03 */ function getLookupValues( childFieldArgs, callback ) { disableFormPreLookup( childFieldArgs.formId ); const pendingLookupArgs = { formId: childFieldArgs.formId, fieldId: childFieldArgs.fieldId, parents: childFieldArgs.parents, parentVals: childFieldArgs.parentVals, unique: getAutoId() }; pendingLookupFieldAjax.push({ childFieldArgs: pendingLookupArgs, callback, pending: true }); } function getAutoId() { return autoId++; } /** * Trigger the frm_lookup_options_loaded event on the field div * * @since 2.03.05 * * @param {Object} $fieldDiv */ function triggerLookupOptionsLoaded( $fieldDiv ) { $fieldDiv.trigger( 'frmLookupOptionsLoaded' ); } /** * Select the defatul value in a radio/checkbox field if no value is selected * * @since 2.02.11 * * @param {Object} inputs * @param {Object} childFieldArgs * @param {string} childFieldArgs.inputType * @param {(string|Array)} defaultValue */ function maybeSetDefaultCbRadioValue( childFieldArgs, inputs, defaultValue ) { if ( defaultValue === undefined ) { return; } var currentValue = false; if ( childFieldArgs.inputType === 'radio' ) { currentValue = getValueFromRadioInputs( inputs ); } else { currentValue = getValuesFromCheckboxInputs( inputs ); } if ( currentValue !== false || inputs.length < 1 ) { return; } var inputName = inputs[ 0 ].name; setCheckboxOrRadioDefaultValue( inputName, defaultValue ); } /** * Hide a Radio Lookup field if it doesn't have any options * * @since 2.01.01 * @param {Object} childFieldArgs * @param {string} childFieldArgs.formId * @param {object} childDiv */ function maybeHideRadioLookup( childFieldArgs, childDiv ) { if ( isFieldConditionallyHidden( childDiv.id, childFieldArgs.formId ) ) { return; } hideFieldContainer( childDiv.id ); addToHideFields( childDiv.id, childFieldArgs.formId ); } /** * Show a radio Lookup field if it has options and conditional logic says it should be shown * * @since 2.01.01 * @param {Object} childFieldArgs * @param {string} childFieldArgs.formId * @param {string} childFieldArgs.fieldId * @param {string} childFieldArgs.repeatRow * @param {object} childDiv * @return {void} */ function maybeShowRadioLookup( childFieldArgs, childDiv ) { if ( isFieldCurrentlyShown( childDiv.id, childFieldArgs.formId ) ) { return; } var logicArgs = getRulesForSingleField( childFieldArgs.fieldId ); if ( logicArgs === false || logicArgs.conditions.length < 1 ) { removeFromHideFields( childDiv.id, childFieldArgs.formId ); showFieldContainer( childDiv.id ); } else { logicArgs.containerId = childDiv.id; logicArgs.repeatRow = childFieldArgs.repeatRow; hideOrShowSingleField( logicArgs ); } } /** * Get new value for a text field if all Lookup Field parents have a value * * @since 2.01.0 * @param {Object} childFieldArgs * @param {string} childFieldArgs.formId * @param {Array} childFieldArgs.parents * @param {Array} childFieldArgs.parentVals * @param {string} childFieldArgs.fieldKey * @param {string} childFieldArgs.fieldId * @param {object} childInput */ function maybeInsertValueInFieldWatchingLookup( childFieldArgs, childInput ) { if ( isChildInputConditionallyHidden( childInput, childFieldArgs.formId ) ) { // TODO: What if field is in conditionally hidden section? checkQueueAfterLookupCompleted( childInput.id ); return; } if ( childFieldArgs.parentVals === false ) { // If any parents have blank values, set the field value to the default value var newValue = childInput.getAttribute( 'data-frmval' ); if ( newValue === null ) { newValue = ''; } insertValueInFieldWatchingLookup( childFieldArgs, childInput, newValue ); checkQueueAfterLookupCompleted( childInput.id ); } else { // If all parents have values, check for a new value disableFormPreLookup( childFieldArgs.formId ); postToAjaxUrl( getFormById( childFieldArgs.formId ), { action: 'frm_get_lookup_text_value', parent_fields: childFieldArgs.parents, parent_vals: childFieldArgs.parentVals, field_id: childFieldArgs.fieldId, nonce: frm_js.nonce }, ( newValue ) => { if ( ! isChildInputConditionallyHidden( childInput, childFieldArgs.formId ) && childInput.value != newValue ) { insertValueInFieldWatchingLookup( childFieldArgs.fieldKey, childInput, newValue ); } enableFormAfterLookup( childFieldArgs.formId ); checkQueueAfterLookupCompleted( childInput.id ); } ); } } /** * Check if the current Lookup watcher field has a queue * * @since 2.03.05 * * @param {string} elementId * @returns {boolean} */ function currentLookupHasQueue( elementId ) { return ( elementId in lookupQueues && lookupQueues[ elementId ].length > 0 ); } /** * Add the current Lookup watcher to a queue of size two * * @since 2.03.05 * * @param {Object} childFieldArgs * @param {Object} childInput */ function addLookupToQueueOfTwo( childFieldArgs, childInput ) { var elementId = childInput.id; if ( elementId in lookupQueues ) { if ( lookupQueues[ elementId ].length >= 2 ) { lookupQueues[ elementId ] = lookupQueues[ elementId ].slice( 0, 1 ); } } else { lookupQueues[ elementId ] = []; } lookupQueues[ elementId ].push({ childFieldArgs, childInput }); } /** * Check the lookupQueue after a value lookup is completed * * @since 2.03.05 * * @param {string} elementId */ function checkQueueAfterLookupCompleted( elementId ) { removeLookupFromQueue( elementId ); doNextItemInLookupQueue( elementId ); } /** * Remove a Lookup from the queue * * @since 2.03.05 * * @param {string} elementId */ function removeLookupFromQueue( elementId ) { lookupQueues[ elementId ].shift(); } /** * Check the current Lookup queue * * @since 2.03.05 * * @param {string} elementId * @return {void} */ function doNextItemInLookupQueue( elementId ) { if ( ! currentLookupHasQueue( elementId ) ) { return; } const childFieldArgs = lookupQueues[ elementId ][ 0 ].childFieldArgs; const childInput = lookupQueues[ elementId ][ 0 ].childInput; updateParentValsForLookup( childInput.name, childFieldArgs ); maybeInsertValueInFieldWatchingLookup( childFieldArgs, childInput ); } /** * Ensures Lookup field parentVals are updated for the current field. * Fixes issue #5611 * * @since 6.19 * * @param {string} childInputName * @param {object} childFieldArgs * * @returns {void} */ function updateParentValsForLookup( childInputName, childFieldArgs ) { if ( ! childInputName ) { return; } childFieldArgs.repeatRow = getRepeatArgsFromFieldName( childInputName ).repeatRow; childFieldArgs.parentVals = getParentLookupFieldVals( childFieldArgs ); } /** * Convert html entities back into their character values * * @param {string} string * @returns {string} */ function decodeEntities( string ) { var decoded = string.replace( /&/g, '&' ) .replace( /</g, '<' ) .replace( />/g, '>' ) .replace( /"/g, '"' ) .replace( /'/g, '\'' ); return decoded; } /** * Insert a new text field Lookup value * * @since 2.01.0 * @param {string} fieldKey * @param {object} childInput * @param {string} newValue */ function insertValueInFieldWatchingLookup( fieldKey, childInput, newValue ) { childInput.value = decodeEntities( newValue ); triggerChange( jQuery( childInput ), fieldKey ); } /** * Add the repeat Row to the child field args * * @since 2.01.0 * @param {string} fieldName * @param {Object} childFieldArgs */ function addRepeatRowForInput( fieldName, childFieldArgs ) { var repeatArgs = getRepeatArgsFromFieldName( fieldName ); if ( repeatArgs.repeatRow !== '' ) { childFieldArgs.repeatRow = repeatArgs.repeatRow; } else { childFieldArgs.repeatRow = ''; } } /******************************************************* Dynamic Field Functions *******************************************************/ // Update a Dynamic field's data or options function updateDynamicField( depFieldArgs, onCurrentPage ) { var depFieldArgsCopy = cloneObjectForDynamicFields( depFieldArgs ); if ( depFieldArgsCopy.inputType === 'data' ) { updateDynamicListData( depFieldArgsCopy, onCurrentPage ); } else if ( onCurrentPage ) { // Only update the options if field is on the current page updateDynamicFieldOptions( depFieldArgsCopy ); } } /** * Clone the depFieldArgs object for use in ajax requests * * @since 2.01.0 * @param {Object} depFieldArgs * @param {string|Array} depFieldArgs.dataLogic.actualValue * @param {string} depFieldArgs.fieldId * @param {string} depFieldArgs.fieldKey * @param {string} depFieldArgs.formId * @param {string} depFieldArgs.containerId * @param {string} depFieldArgs.repeatRow * @param {string} depFieldArgs.inputType * @return {Object} dynamicFieldArgs */ function cloneObjectForDynamicFields( depFieldArgs ) { var dataLogic = { actualValue: depFieldArgs.dataLogic.actualValue, fieldId: depFieldArgs.dataLogic.fieldId }; var dynamicFieldArgs = { fieldId: depFieldArgs.fieldId, fieldKey: depFieldArgs.fieldKey, formId: depFieldArgs.formId, containerId: depFieldArgs.containerId, repeatRow: depFieldArgs.repeatRow, dataLogic, children: '', inputType: depFieldArgs.inputType }; return dynamicFieldArgs; } pendingDynamicFieldAjax = []; /** * Update a Dynamic List field * * @since 2.01 * @param {Object} depFieldArgs * @param {string} depFieldArgs.containerId * @param {string|Array} depFieldArgs.dataLogic.actualValue * @param {string} depFieldArgs.fieldId */ function updateDynamicListData( depFieldArgs, onCurrentPage ) { var $fieldDiv; if ( onCurrentPage ) { $fieldDiv = jQuery( '#' + depFieldArgs.containerId ); addLoadingIcon( $fieldDiv ); } pendingDynamicFieldAjax.push({ args: { depFieldArgs, onCurrentPage }, data: { entry_id: depFieldArgs.dataLogic.actualValue, current_field: depFieldArgs.fieldId, hide_id: depFieldArgs.containerId, on_current_page: onCurrentPage, nonce: frm_js.nonce } }); } /** * Update a Dynamic dropdown, radio, or checkbox options * * @since 2.01 * @param {Object} depFieldArgs * @param {string} depFieldArgs.containerId * @param {string} depFieldArgs.dataLogic.fieldId * @param {string|Array} depFieldArgs.dataLogic.actualValue * @param {string} depFieldArgs.fieldId */ function updateDynamicFieldOptions( depFieldArgs ) { var $fieldDiv = jQuery( '#' + depFieldArgs.containerId ), $fieldInputs = $fieldDiv.find( 'select[name^="item_meta"], input[name^="item_meta"]' ), prevValue = getFieldValueFromInputs( $fieldInputs ), defaultVal = $fieldInputs.data( 'frmval' ), editingEntry = $fieldDiv.closest( 'form' ).find( 'input[name="id"]' ).val(); addLoadingIcon( $fieldDiv ); postToAjaxUrl( getFormById( depFieldArgs.formId ), { action: 'frm_fields_ajax_data_options', trigger_field_id: depFieldArgs.dataLogic.fieldId, entry_id: depFieldArgs.dataLogic.actualValue, field_id: depFieldArgs.fieldId, default_value: defaultVal, container_id: depFieldArgs.containerId, editing_entry: editingEntry, prev_val: prevValue, nonce: frm_js.nonce }, ( html ) => { var $optContainer = $fieldDiv.find( '.frm_opt_container, .frm_data_container' ); $optContainer.html( html ); var $dynamicFieldInputs = $optContainer.find( 'select, input[type="checkbox"], input[type="radio"]' ); removeLoadingIcon( $optContainer ); if ( html === '' || $dynamicFieldInputs.length < 1 ) { hideDynamicField( depFieldArgs ); } else { var valueChanged = dynamicFieldValueChanged( depFieldArgs, $dynamicFieldInputs, prevValue ); showDynamicField( depFieldArgs, $fieldDiv, $dynamicFieldInputs, valueChanged ); } } ); } function dynamicFieldValueChanged( depFieldArgs, $dynamicFieldInputs, prevValue ) { var newValue = getFieldValueFromInputs( $dynamicFieldInputs ); return ( prevValue !== newValue ); } /** * Update the value in a hidden Dynamic List field * * @since 2.01.01 * @param {Object} depFieldArgs * @param {string} depFieldArgs.fieldKey * @param {string} depFieldArgs.repeatRow * @param {string} depFieldArgs.containerId * @param {string} depFieldArgs.formId */ function updateHiddenDynamicListField( depFieldArgs, newValue ) { // Get the Dynamic List input var inputId = 'field_' + depFieldArgs.fieldKey; if ( depFieldArgs.repeatRow !== '' ) { inputId += '-' + depFieldArgs.repeatRow; } var listInput = document.getElementById( inputId ); if ( listInput === null ) { return; } // Set the new value listInput.value = newValue; // Remove field from hidden field list if ( isFieldConditionallyHidden( depFieldArgs.containerId, depFieldArgs.formId ) ) { removeFromHideFields( depFieldArgs.containerId, depFieldArgs.formId ); } triggerChange( jQuery( listInput ) ); } // Add the loading icon with jQuery function addLoadingIcon( $fieldDiv ) { var currentHTML = $fieldDiv.html(); if ( currentHTML.includes('frm-loading-img') ) { // Loading image already present } else { var loadingIcon = ''; $fieldDiv.html( currentHTML + loadingIcon ); var $optContainer = $fieldDiv.find( '.frm_opt_container, .frm_data_container' ); $optContainer.hide(); } } // Add the loading icon with JavaScript function addLoadingIconJS( fieldDiv, optContainer ) { var currentHTML = fieldDiv.innerHTML; if ( currentHTML.includes('frm-loading-img') ) { // Loading image already present } else { optContainer.classList.add( 'frm_hidden' ); var loadingIcon = document.createElement( 'span' ); loadingIcon.setAttribute( 'class', 'frm-loading-img' ); fieldDiv.insertBefore( loadingIcon, optContainer.nextSibling ); } } // Remove the loading icon with jQuery function removeLoadingIcon( $optContainer ) { $optContainer.parent().children( '.frm-loading-img' ).remove(); $optContainer.show(); } // Remove the loading icon with JavaScript function removeLoadingIconJS( fieldDiv, optContainer ) { var loadingIcon = fieldDiv.getElementsByClassName( 'frm-loading-img' )[ 0 ]; if ( loadingIcon !== null && loadingIcon !== undefined ) { loadingIcon.parentNode.removeChild( loadingIcon ); } optContainer.classList.remove( 'frm_hidden' ); } // Get the field value from all the inputs function getFieldValueFromInputs( $inputs ) { var fieldValue = [], currentValue = ''; $inputs.each( function() { currentValue = this.value; if ( this.type === 'radio' || this.type === 'checkbox' ) { if ( this.checked === true ) { fieldValue.push( currentValue ); } } else if ( currentValue !== '' ) { fieldValue.push( currentValue ); } } ); if ( fieldValue.length === 0 ) { fieldValue = ''; } return fieldValue; } // Hide and clear a Dynamic Field function hideDynamicField( depFieldArgs ) { hideFieldAndClearValue( depFieldArgs, true ); } // Show Dynamic field function showDynamicField( depFieldArgs, $fieldDiv, $fieldInputs, valueChanged ) { if ( isFieldConditionallyHidden( depFieldArgs.containerId, depFieldArgs.formId ) ) { removeFromHideFields( depFieldArgs.containerId, depFieldArgs.formId ); $fieldDiv.show(); } if ( $fieldInputs.hasClass( 'frm_chzn' ) || $fieldInputs.hasClass( 'frm_slimselect' ) ) { loadAutocomplete(); } if ( valueChanged === true ) { triggerChange( $fieldInputs ); } } /************************************************* Calculations ************************************************/ function triggerCalc() { if ( typeof __FRMCALC === 'undefined' ) { // there are no calculations on this page return; } var triggers = __FRMCALC.triggers; if ( triggers ) { jQuery( triggers.join() ).trigger({ type: 'change', selfTriggered: true }); } triggerCalcWithoutFields(); } function triggerCalcWithoutFields() { var calcs = __FRMCALC.calc, vals = []; for ( var fieldKey in calcs ) { if ( calcs[ fieldKey ].fields.length < 1 && calcs[ fieldKey ].calc && '0' !== calcs[ fieldKey ].calc ) { var totalField = document.getElementById( 'field_' + fieldKey ); if ( totalField !== null && ! isChildInputConditionallyHidden( totalField, calcs[ fieldKey ].form_id ) ) { // if field is not hidden, do calculation doSingleCalculation( __FRMCALC, fieldKey, vals ); } } } } function doCalculation( fieldId, triggerField ) { if ( typeof __FRMCALC === 'undefined' ) { // there are no calculations on this page return; } var allCalcs = __FRMCALC, calc = allCalcs.fields[ fieldId ], vals = []; if ( calc === undefined ) { // this field is not used in a calculation return; } var keys = calc.total; var len = keys.length; var pages = getStartEndPage( allCalcs.calc[ keys[ 0 ] ]); // loop through each calculation this field is used in for ( var i = 0, l = len; i < l; i++ ) { var totalOnPage = isTotalFieldOnPage( allCalcs.calc[ keys[ i ] ], pages ); // Proceed with calculation if total field is not conditionally hidden if ( totalOnPage && isTotalFieldConditionallyHidden( allCalcs.calc[ keys[ i ] ], triggerField.attr( 'name' ) ) === false ) { doSingleCalculation( allCalcs, keys[ i ], vals, triggerField ); } } } /** * @param thisField * @since 2.05.06 */ function getStartEndPage( thisField ) { var formId = thisField.form_id, formContainer = document.getElementById( 'frm_form_' + formId + '_container' ); if ( formContainer === null && thisField.in_section ) { var fieldContainer = document.getElementById( 'frm_field_' + thisField.in_section + '_container' ); if ( fieldContainer === null ) { // bail now and run the calculation if we can't determine if it's on the page return []; } formContainer = closest( fieldContainer, ( el ) => { return el.tagName === 'FORM'; }); formId = formContainer.elements.namedItem( 'form_id' ).value; } var hasPreviousPage = formContainer.getElementsByClassName( 'frm_next_page' ); var hasAnotherPage = document.getElementById( 'frm_page_order_' + formId ); var pages = []; if ( hasPreviousPage.length > 0 ) { pages.start = hasPreviousPage[ 0 ]; } if ( hasAnotherPage !== null ) { pages.end = hasAnotherPage; } return pages; } function closest( el, fn ) { return el && ( fn( el ) ? el : closest( el.parentNode, fn ) ); } /** * If the total field is not on the current page, don't trigger the calculation * * @param calcDetails * @param pages * @since 2.05.06 */ function isTotalFieldOnPage( calcDetails, pages ) { if ( pages.start !== undefined || pages.end !== undefined ) { // the form has pages var hiddenTotalField = jQuery( 'input[type=hidden][name*="[' + calcDetails.field_id + ']"]' ); if ( hiddenTotalField.length ) { // the total field is a hidden field return isHiddenTotalOnPage( hiddenTotalField, pages ); } } return true; } function isHiddenTotalOnPage( hiddenTotalField, pages ) { var onPage, hiddenParent = hiddenTotalField.closest( '.frm_form_field' ); if ( hiddenParent.length ) { // the field is in a section return true; } var totalPos = hiddenTotalField.index(); var isAfterStart = true; var isBeforeEnd = true; if ( pages.start !== undefined ) { isAfterStart = jQuery( pages.start ).index() < totalPos; } if ( pages.end !== undefined ) { isBeforeEnd = jQuery( pages.end ).index() > totalPos; } onPage = ( isAfterStart && isBeforeEnd ); if ( ! onPage ) { onPage = hiddenTotalField.closest( '.do-calculation' ).length > 0; } return onPage; } /** * Check if a total field is conditionally hidden * @param {Object} calcDetails * @param {string} calcDetails.field_id * @param {string} calcDetails.form_id * @param {string} calcDetails.inSection * @param {string} calcDetails.inEmbedForm * @param {string} triggerFieldName * @returns {boolean} */ function isTotalFieldConditionallyHidden( calcDetails, triggerFieldName ) { var hidden = false, fieldId = calcDetails.field_id, formId = calcDetails.form_id, hiddenFields = getHiddenFields( formId ); // Check if there are any conditionally hidden fields if ( hiddenFields.length < 1 ) { return hidden; } if ( calcDetails.inSection === '0' && calcDetails.inEmbedForm === '0' ) { // Field is not in a section or embedded form hidden = isNonRepeatingFieldConditionallyHidden( fieldId, hiddenFields ); } else { // Field is in a section or embedded form var repeatArgs = getRepeatArgsFromFieldName( triggerFieldName ); if ( isNonRepeatingFieldConditionallyHidden( fieldId, hiddenFields ) ) { // Check standard field hidden = true; } else if ( isRepeatingFieldConditionallyHidden( fieldId, repeatArgs, hiddenFields ) ) { // Check repeating field hidden = true; } else if ( calcDetails.inSection !== '0' && calcDetails.inEmbedForm !== '0' ) { // Check section in embedded form hidden = isRepeatingFieldConditionallyHidden( calcDetails.inSection, repeatArgs, hiddenFields ); } else if ( calcDetails.inSection !== '0' ) { // Check section hidden = isNonRepeatingFieldConditionallyHidden( calcDetails.inSection, hiddenFields ); } else if ( calcDetails.inEmbedForm !== '0' ) { // Check embedded form hidden = isNonRepeatingFieldConditionallyHidden( calcDetails.inEmbedForm, hiddenFields ); } } return hidden; } // Check if a non-repeating field is conditionally hidden function isNonRepeatingFieldConditionallyHidden( fieldId, hiddenFields ) { var htmlID = 'frm_field_' + fieldId + '_container'; return ( hiddenFields.includes(htmlID) ); } // Check if a repeating field is conditionally hidden function isRepeatingFieldConditionallyHidden( fieldId, repeatArgs, hiddenFields ) { var hidden = false; if ( repeatArgs.repeatingSection ) { var fieldRepeatId = 'frm_field_' + fieldId + '-' + repeatArgs.repeatingSection; fieldRepeatId += '-' + repeatArgs.repeatRow + '_container'; hidden = ( hiddenFields.includes(fieldRepeatId) ); } return hidden; } function maybeShowCalculationsErrorAlert( err, fieldKey, thisFullCalc ) { var alertMessage = ''; if ( ( ! jQuery( 'form' ).hasClass( 'frm-admin-viewing' ) ) ) { return; } alertMessage += frm_js.calc_error + ' ' + fieldKey + ':\n\n'; alertMessage += thisFullCalc + '\n\n'; if ( err.message ) { alertMessage += err.message + '\n\n'; } alert( alertMessage ); } /** * Adjust a Date for timezone offset * * @param {Date} date * @returns {Date} */ function treatAsUTC( date ) { var copy = new Date( date.valueOf() ); copy.setMinutes( copy.getMinutes() - copy.getTimezoneOffset() ); return copy; } /** * Try to change date strings and ints to Date objects before calculations * * @param {mixed} date * @returns {mixed} */ function normalizeDate( date ) { switch ( typeof date ) { case 'number': return new Date( date * 86400000 ); // 1000 * 60 * 60 * 24 (milliseconds per day) case 'string': return new Date( date ); default: return date; } } /** * Calculate the difference between two dates. * This is triggered by the [age] and [date_calc] shortcodes. * * @param {mixed} a Field date. * @param {mixed} b Compare date. * @param {string} format Either 'days' or 'years'. * @param {number|undefined} fieldId This is defined by the datepicker add-on. * @param {number|undefined} compareId This is defined by the datepicker add-on. * @returns {int} */ function calculateDateDifference( a, b, format, fieldId, compareId ) { a = normalizeDate( a ); b = normalizeDate( b ); switch ( format ) { case 'days': { if ( 'function' === typeof window.frmCalcDateDifferenceDays && fieldId && compareId ) { return window.frmCalcDateDifferenceDays( treatAsUTC( a ), treatAsUTC( b ), fieldId, compareId ); } return Math.floor( ( treatAsUTC( b ) - treatAsUTC( a ) ) / 86400000 ); // 1000 * 60 * 60 * 24 (milliseconds per day) } case 'years': default: { var years = b.getFullYear() - a.getFullYear(); if ( b.getMonth() < a.getMonth() || b.getMonth() === a.getMonth() && b.getDate() < a.getDate() ) { years--; } return years; } } } /** * @param {object} triggerField * @param {string} fieldKey * * @returns {object} */ function getTotalOrCalcField( triggerField, fieldKey ) { const form = document.contains( triggerField[0] ) ? triggerField.closest( 'form' ) : jQuery( `#frm_field_${triggerField[0].dataset.sectionid}_container` ).closest( 'form' ); return form.find( fieldKey ); } /** * Set value on a field, properly handling IMask instances. * IMask handles formatting (leading zeros, fixed literals, etc.) natively * when value is set through the mask instance. * * @since 6.23 * * @param {jQuery} field The field element * @param {string|number} value The value to set * @returns {void} */ function setFieldValueWithMask( field, value ) { const fieldElement = field.get( 0 ); if ( fieldElement && frmMaskInstances.has( fieldElement ) ) { const padded = padValueForMask( String( value ), fieldElement.dataset.frmmask || '' ); frmMaskInstances.get( fieldElement ).value = padded; } else { field.val( value ); } } /** * Pad a numeric value so it fills all digit slots in a mask pattern. * IMask processes characters left-to-right and cannot pad on its own, * so e.g. value "5" with mask "00.00" needs to become "05.00". * * Non-numeric values or masks without digit slots are returned as-is. * * @since 6.23 * * @param {string} value The string value to pad. * @param {string} mask The mask pattern (e.g. "00", "00.00", "P0"). * @returns {string} */ function padValueForMask( value, mask ) { if ( ! mask ) { return value; } // Strip non-digit/non-decimal/non-minus characters from the value for counting. const numericValue = value.replace( /[^0-9.\-]/g, '' ); if ( '' === numericValue || isNaN( Number( numericValue ) ) ) { return value; } // Split the mask into parts around the literal "." if present. const maskParts = mask.split( '.' ); const intMask = maskParts[0]; const decMask = maskParts.length > 1 ? maskParts.slice( 1 ).join( '.' ) : ''; // Count only digit-slot characters (0 and 9) in each part. const intSlots = ( intMask.match( /[09]/g ) || [] ).length; const decSlots = ( decMask.match( /[09]/g ) || [] ).length; if ( 0 === intSlots && 0 === decSlots ) { return value; } // Split the numeric value around the decimal point. const valueParts = numericValue.split( '.' ); let intValue = ( valueParts[0] || '0' ).replace( /^-/, '' ); const isNegative = numericValue.startsWith( '-' ); // Pad integer part with leading zeros. if ( intSlots > intValue.length ) { intValue = intValue.padStart( intSlots, '0' ); } let result = ( isNegative ? '-' : '' ) + intValue; // Pad decimal part with trailing zeros. if ( decSlots > 0 ) { let decValue = valueParts[1] || ''; decValue = decValue.padEnd( decSlots, '0' ).substring( 0, decSlots ); result += '.' + decValue; } return result; } function doSingleCalculation( allCalcs, fieldKey, vals, triggerField ) { var currency, total, dec, updatedTotal, totalField, thisCalc = allCalcs.calc[ fieldKey ], thisFullCalc = thisCalc.calc, fieldInfo = { triggerField, inSection: false, thisFieldCall: 'input[id^="field_' + fieldKey + '-"]' }; if ( triggerField !== undefined && triggerField && triggerField.length ) { totalField = getTotalOrCalcField( triggerField, '#field_' + fieldKey ); } else { totalField = jQuery( document.getElementById( 'field_' + fieldKey ) ); } if ( totalField.get( 0 )?.dataset.minGap ) { // Avoid running calculation for Range sliders. return; } // exit early for sliders as there is no calculation for such types and avoid errors that might rise due to formatting the value if ( totalField.attr( 'type' ) === 'range' ) { return; } // TODO: update this to work more like conditional logic if ( totalField.length < 1 && triggerField !== undefined ) { // check if the total field is inside of a repeating/embedded form fieldInfo.inSection = true; fieldInfo.thisFieldId = objectSearch( allCalcs.fieldsWithCalc, fieldKey ); totalField = getSiblingField( fieldInfo ); } if ( totalField === null || totalField.length < 1 ) { return; } // loop through the fields in this calculation thisFullCalc = getValsForSingleCalc( thisCalc, thisFullCalc, allCalcs, vals, fieldInfo ); total = ''; dec = ''; if ( 'function' === typeof window[ 'frmProGetCalcTotal' + thisCalc.calc_type ]) { total = window[ 'frmProGetCalcTotal' + thisCalc.calc_type ].call( thisCalc, thisFullCalc, { totalField }); } else if ( thisCalc.calc_type === 'text' ) { total = thisFullCalc; } else { if ( 'date' === thisCalc.calc_type ) { // If the datepicker JS did not load, skip date calculations. return; } // Set the number of decimal places dec = thisCalc.calc_dec; // allow .toFixed for reverse compatibility if ( thisFullCalc.includes(').toFixed(') ) { var calcParts = thisFullCalc.split( ').toFixed(' ); if ( isNumeric( calcParts[ 1 ]) ) { dec = calcParts[ 1 ]; thisFullCalc = thisFullCalc.replace( ').toFixed(' + dec, '' ); } } thisFullCalc = trimNumericCalculation( thisFullCalc ); if ( thisFullCalc !== '' ) { try { total = parseFloat( eval( thisFullCalc ) ); } catch ( err ) { maybeShowCalculationsErrorAlert( err, fieldKey, thisFullCalc ); } } if ( total === undefined || isNaN( total ) ) { total = 0; } // Set decimal points if ( isNumeric( dec ) && total !== '' ) { total = total.toFixed( dec ); } } if ( thisCalc.is_currency === true && isNumeric( total ) ) { currency = getCurrencyFromCalcRule( thisCalc ); if ( currency.decimals > 0 ) { total = Math.round10( total, currency.decimals ); total = maybeAddTrailingZeroToPrice( total, currency ); dec = currency.decimals; } } if ( totalField.val() === total ) { setDisplayedTotal( totalField, total, currency ); return; } updatedTotal = false; if ( ( isNumeric( dec ) || thisCalc.is_currency ) && [ 'number', 'text' ].includes(totalField.attr( 'type' )) ) { if ( total.toString().slice( -1 ) == '0' && navigator.userAgent.toLowerCase().includes('firefox') ) { // Change the input to text in Firefox. Otherwise, trailing decimals will fail. totalField[0].setAttribute( 'type', 'text' ); } if ( totalField.parent().is( '.frm_with_box.frm_hidden' ) && 'string' === typeof total ) { updatedTotal = true; setFieldValueWithMask( totalField, total.replace( ',', '.' ) ); } } if ( ! updatedTotal ) { // Set value using proper mask handling setFieldValueWithMask( totalField, total ); } triggerEvent( document, 'frmCalcUpdatedTotal', { totalField, total }); if ( triggerField === null || triggerField === undefined || totalField.attr( 'name' ) != triggerField.attr( 'name' ) ) { totalField.trigger({ type: 'change', selfTriggered: true, frmTriggered: fieldKey }); } setDisplayedTotal( totalField, total, currency ); } /** * Show the total for frm_total fields. */ function setDisplayedTotal( totalField, total, currency ) { var prepend, append, showTotal = totalField.parent().prev(); if ( ! showTotal.hasClass( 'frm_total_formatted' ) ) { return; } prepend = showTotal.data( 'prepend' ); append = showTotal.data( 'append' ); if ( prepend === undefined ) { prepend = ''; } if ( append === undefined ) { append = ''; } if ( typeof currency === 'object' ) { total = formatCurrency( total, currency ); if ( currency.symbol_left === prepend ) { prepend = ''; } if ( currency.symbol_right === append ) { append = ''; } } if ( prepend !== '' ) { prepend = '' + prepend + ''; } if ( append !== '' ) { append = '' + append + ''; } showTotal.html( prepend + '' + total + '' + append ); } function getValsForSingleCalc( thisCalc, thisFullCalc, allCalcs, vals, fieldInfo ) { var fCount, f, field, date, findVar; fCount = thisCalc.fields.length; for ( f = 0; f < fCount; f++ ) { field = { triggerField: fieldInfo.triggerField, thisFieldId: thisCalc.fields[ f ], inSection: fieldInfo.inSection, valKey: fieldInfo.inSection + '' + thisCalc.fields[ f ], thisField: allCalcs.fields[ thisCalc.fields[ f ] ], thisFieldCall: 'input' + allCalcs.fieldKeys[ thisCalc.fields[ f ] ], formID: thisCalc.form_id }; field = getCallForField( field, allCalcs ); if ( ! thisCalc.calc_type ) { // Math calc type. field.valKey = 'num' + field.valKey; vals = getCalcFieldId( field, allCalcs, vals ); if ( vals[ field.valKey ] === undefined || isNaN( vals[ field.valKey ]) ) { vals[ field.valKey ] = 0; if ( field.thisField.type === 'date' ) { date = tryToGetDateValue( field ); if ( null !== date ) { vals[ field.valKey ] = Math.floor( date.getTime() / 86400000 ); } else { thisFullCalc = ''; } } } else if ( 0 === vals[ field.valKey ] && field.thisField.type === 'date' && dateValueShouldBeClearedForDateCalculation( field, fieldInfo ) ) { thisFullCalc = ''; } } else { // Text calc type. field.valKey = 'text' + field.valKey; vals = getTextCalcFieldId( field, vals ); if ( vals[ field.valKey ] === undefined ) { vals[ field.valKey ] = ''; } } if ( thisCalc.calc_type === 'text' ) { thisFullCalc = replaceShortcodesWithShowOptions( thisFullCalc, vals, field ); } findVar = '[' + field.thisFieldId + ']'; findVar = findVar.replace( /([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1' ); thisFullCalc = thisFullCalc.replace( new RegExp( findVar, 'g' ), vals[ field.valKey ]); } return thisFullCalc; } /** * Support show=label (for field with options), show=first and show=last (for name fields) * in text calculations. * * @param {String} fullCalc * @param {Array} vals * @param {Object} field * @returns {String} */ function replaceShortcodesWithShowOptions( fullCalc, vals, field ) { fullCalc = replaceShowShortcode( fullCalc, vals, field, 'label', () => { return getOptionLabelsFromValues( vals[ field.valKey ], field ); } ); Array.prototype.forEach.call( [ 'first', 'middle', 'last' ], ( nameFieldPart ) => { fullCalc = replaceNameShortcode( fullCalc, vals, field, nameFieldPart ); } ); return fullCalc; } function replaceNameShortcode( fullCalc, vals, field, show ) { var valueCallback = function() { var match = false; document.querySelectorAll( field.thisFieldCall ).forEach( ( input ) => { if ( show === input.id.substr( -show.length ) ) { match = input; } } ); return match ? match.value : ''; }; return replaceShowShortcode( fullCalc, vals, field, show, valueCallback ); } function replaceShowShortcode( fullCalc, vals, field, show, valueCallback ) { var findVar; findVar = '[' + field.thisFieldId + ' show=' + show + ']'; if ( -1 === fullCalc.indexOf( findVar ) ) { return fullCalc; } vals[ field.valKey + show ] = valueCallback(); findVar = findVar.replace( /([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1' ); fullCalc = fullCalc.replace( new RegExp( findVar, 'g' ), vals[ field.valKey + show ]); return fullCalc; } function tryToGetDateValue( field ) { var $element = jQuery( field.thisField.key ); return $element.hasClass( 'hasDatepicker' ) ? $element.datepicker( 'getDate' ) : null; } /** * Check if a date value is ready for use in a calculation. */ function dateValueShouldBeClearedForDateCalculation( field, fieldInfo ) { if ( fieldInfo.triggerField !== null ) { if ( fieldInfo.triggerField.is( 'input' ) ) { if ( datepickerFieldShouldBeClearedForDateCalculation( fieldInfo.triggerField ) ) { return fieldShouldBeClearedForDateCalculation( field.thisFieldCall, field.thisField.key ); } return '' === fieldInfo.triggerField.val(); } return fieldInfo.triggerField.is( 'div' ) && fieldInfo.triggerField.end().is( 'input' ) && '' === fieldInfo.triggerField.end().val(); } else if ( fieldShouldBeClearedForDateCalculation( field.thisFieldCall, field.thisField.key ) ) { return true; } return false; } function datepickerFieldShouldBeClearedForDateCalculation( field ) { var dateValue = field.hasClass( 'hasDatepicker' ) ? field.datepicker( 'getDate' ) : null; return null !== dateValue && -72000000 !== dateValue.getTime(); } function fieldShouldBeClearedForDateCalculation( fieldCall, fieldKey ) { return 0 === fieldCall.indexOf( 'input' ) && 0 === fieldKey.indexOf( '[id=' ) && '' === jQuery( fieldKey ).val(); } function getOptionLabelsFromValues( value, field ) { var fieldId, options, split, labels, length, index; fieldId = field.thisFieldId; if ( 'undefined' === typeof __FRMCALC.options || 'undefined' === typeof __FRMCALC.options[ fieldId ]) { return value; } options = __FRMCALC.options[ fieldId ]; if ( 'checkbox' === field.thisField.type ) { split = value.split( ', ' ); labels = []; length = split.length; for ( index = 0; index < length; ++index ) { if ( 'undefined' !== typeof options[ split[ index ] ]) { labels.push( options[ split[ index ] ]); } } return labels.join( ', ' ); } return 'undefined' !== typeof options[ value ] ? options[ value ] : ''; } /** * Trim non-numeric characters from the end of a numeric calculation * * @since 2.03.02 * @param {String} numericCalc * @returns {String} */ function trimNumericCalculation( numericCalc ) { var lastChar = numericCalc.charAt( numericCalc.length - 1 ); if ( lastChar === '+' || lastChar === '-' ) { numericCalc = numericCalc.substr( 0, numericCalc.length - 1 ); } return numericCalc; } function getCallForField( field, allCalcs ) { if ( field.thisField.type === 'checkbox' || field.thisField.type === 'radio' || field.thisField.type === 'scale' || field.thisField.type === 'star' ) { field.thisFieldCall = field.thisFieldCall + ':checked,' + field.thisFieldCall + '[type=hidden]'; } else if ( field.thisField.type === 'select' || field.thisField.type === 'time' ) { field.thisFieldCall = 'select' + allCalcs.fieldKeys[ field.thisFieldId ] + ' option:selected,' + field.thisFieldCall + '[type=hidden]'; } else if ( field.thisField.type === 'textarea' ) { field.thisFieldCall = field.thisFieldCall + ',textarea' + allCalcs.fieldKeys[ field.thisFieldId ]; } return field; } function maybeDoCalcForSingleField( fieldInput ) { if ( typeof __FRMCALC === 'undefined' ) { // there are no calculations on this page return; } // Exit now if field is a type that can't do calculations if ( ! fieldCanDoCalc( fieldInput.type ) ) { return; } var allCalcs = __FRMCALC, fieldKey = getFieldKey( fieldInput.id, fieldInput.name ), triggerField = maybeGetTriggerField( fieldInput ), vals = []; if ( allCalcs.calc[ fieldKey ] === undefined ) { // This field doesn't have any calculations return; } doSingleCalculation( allCalcs, fieldKey, vals, triggerField ); } function fieldCanDoCalc( fieldType ) { return -1 !== [ 'text', 'hidden', 'number', 'textarea' ].indexOf( fieldType ); } function getFieldKey( fieldHtmlId, fieldName ) { var fieldKey = fieldHtmlId.replace( 'field_', '' ), newFieldKey = ''; if ( isRepeatingFieldByName( fieldName ) ) { var fieldKeyParts = fieldKey.split( '-' ); for ( var i = 0; i < fieldKeyParts.length - 1; i++ ) { if ( newFieldKey === '' ) { newFieldKey = fieldKeyParts[ i ]; } else { newFieldKey = newFieldKey + '-' + fieldKeyParts[ i ]; } } fieldKey = newFieldKey; } return fieldKey; } function maybeGetTriggerField( fieldInput ) { var triggerField = null; if ( isRepeatingFieldByName( fieldInput.name ) ) { if ( fieldInput.type !== 'hidden' ) { triggerField = jQuery( fieldInput ).closest( '.frm_form_field' ); } else { triggerField = jQuery( fieldInput ); } } return triggerField; } function isRepeatingFieldByName( fieldName ) { var fieldNameParts = fieldName.split( '][' ); return fieldNameParts.length >= 3; } function getCalcFieldId( field, allCalcs, vals ) { if ( vals[ field.valKey ] !== undefined && vals[ field.valKey ] !== 0 ) { return vals; } vals[ field.valKey ] = 0; var currency, calcField = getCalcField( field ); if ( calcField === false ) { return vals; } calcField.each( function() { const thisVal = getOptionValue( field.thisField, this ); const hasCustomFormat = this.classList.contains( 'frm-has-number-format' ); if ( field.thisField.type === 'date' ) { var d = getDateFieldValue( allCalcs.date, thisVal ); if ( d !== null ) { vals[ field.valKey ] = Math.ceil( d / ( 1000 * 60 * 60 * 24 ) ); } } else if ( 'data' === field.thisField.type ) { vals[ field.valKey ] = 0; if ( '' !== thisVal ) { if ( 'SELECT' === this.tagName ) { vals[ field.valKey ] = parseFloat( this.querySelector( 'option[value="' + thisVal + '"]' ).textContent ); } else if ( null !== this.closest( '.frm_checkbox' ) ) { vals[ field.valKey ] = 0; jQuery( this.closest( '.frm_opt_container' ).querySelectorAll( 'input:checked' ) ).each( function() { vals[ field.valKey ] += parseFloat( this.parentNode.textContent ); } ); } else { vals[ field.valKey ] = parseFloat( this.parentNode.textContent ); } } } else if ( this.hasAttribute( 'data-frmprice' ) || field.thisField.type === 'total' || hasCustomFormat ) { // data-frmprice means product field. currency = getCurrency( field.formID ); vals[ field.valKey ] += parseFloat( ! currency ? thisVal : ( hasCustomFormat && String( thisVal ).trim().startsWith( '-' ) ? '-' : '' ) + preparePrice( thisVal, currency ) ); } else { var n = thisVal; if ( n !== '' && n !== 0 ) { n = n.trim(); // Include e to support scientific notation like 10e2. n = parseFloat( n.replace( /,/g, '' ).match( /-?[\d\.e]+$/ ) ); } if ( n === undefined || isNaN( n ) || n === '' ) { n = 0; } vals[ field.valKey ] += n; } }); return vals; } function getTextCalcFieldId( field, vals ) { if ( vals[ field.valKey ] !== undefined && vals[ field.valKey ] !== '' ) { return vals; } vals[ field.valKey ] = ''; var calcField = getCalcField( field ); if ( calcField === false ) { return vals; } var count = 0; var sep = ''; calcField.each( function() { var thisVal = getOptionValue( field.thisField, this ); thisVal = thisVal.trim(); sep = getCalcSep( field, count ); if ( thisVal !== '' ) { vals[ field.valKey ] += sep + thisVal; count++; } }); return vals; } function getCalcSep( field, count ) { var sep = ''; if ( count > 0 ) { if ( field.thisField.type === 'time' ) { if ( count == 1 ) { sep = ':'; } else if ( count == 2 ) { sep = ' '; } } else { sep = ', '; } var customSep = jQuery( document ).triggerHandler( 'frmCalSeparation', [ field.thisField, count ]); if ( customSep !== undefined ) { sep = customSep; } } return sep; } function getCalcField( field ) { var calcField; if ( field.inSection === false ) { if ( 'name' === field.thisField.type ) { return getOffScreenFieldForName( field ); } if ( 'undefined' !== typeof field.triggerField && field.triggerField && field.triggerField.length ) { calcField = getTotalOrCalcField( field.triggerField, field.thisFieldCall ); } else { calcField = jQuery( field.thisFieldCall ); } if ( ! calcField.length && -1 !== [ 'date', 'data' ].indexOf( field.thisField.type ) ) { calcField = jQuery( field.thisField.key ); if ( ! calcField.length && 'data' === field.thisField.type ) { // radio and checkbox fields have additional hyphenated values in their ids. calcField = jQuery( field.thisField.key.replace( '="', '^=' ).replace( '"]', '-' ) + ']:checked' ); } } } else { calcField = getSiblingField( field ); } if ( calcField === null || calcField === undefined || calcField.length < 1 ) { calcField = false; } if ( calcField.length > 1 ) { calcField = filterCalcField( calcField, field.thisFieldId ); } return calcField; } /** * Filters calculation fields (elements) to be involved in the current calculation. This is so that elements that are in calcField just * because their id starts with a similar string while they don't relate to the calculation are removed from the list. * * Fixes #1337. * * @param {Object} $calcField A jQuery object that contains a set of `elements to be involved in the current calculation. * @param {String} thisFieldCall The query selector used to select calculation elements. */ function filterCalcField( $calcField, thisFieldId ) { return $calcField.filter( function() { var target = 'OPTION' === this.nodeName ? this.closest( 'select' ) : this; return target && target.name && target.name.includes(thisFieldId); }); } /** * Create an input with both first and last name. * This isn't actually added to the page, but is used for Name calculations. * * @param {Object} field * @returns {Object} */ function getOffScreenFieldForName( field ) { var nameParts, input; nameParts = []; document.querySelectorAll( field.thisFieldCall ).forEach( ( input ) => { nameParts.push( input.value ); } ); input = document.createElement( 'input' ); input.value = nameParts.join( ' ' ); return jQuery( input ); } /** * Get the value from a date field regardless of whether datepicker is defined for it * Limitations: If using a format with a 2-digit date, '20' will be added to the front if the year is prior to 70 */ function getDateFieldValue( dateFormat, thisVal ) { var d = 0; if ( ! thisVal ) { // If no value was selected in date field, use 0 } else if ( jQuery.datepicker === undefined ) { // If date field is not on the current page const splitAt = [ '/', '.' ].find( char => dateFormat.includes( char ) ) || '-'; var year = '', month = '', day = '', formatPieces = dateFormat.split( splitAt ), datePieces = thisVal.split( splitAt ); for ( var i = 0; i < formatPieces.length; i++ ) { if ( formatPieces[ i ] === 'y' ) { var currentYear = new Date().getFullYear() + 15; var currentYearPlusFifteen = currentYear.toString().substr( 2, 2 ); if ( datePieces[ i ] > currentYearPlusFifteen ) { year = '19' + datePieces[ i ]; } else { year = '20' + datePieces[ i ]; } } else if ( formatPieces[ i ] === 'yy' ) { year = datePieces[ i ]; } else if ( formatPieces[ i ] === 'm' || formatPieces[ i ] === 'mm' ) { month = datePieces[ i ]; if ( month.length < 2 ) { month = '0' + month; } } else if ( formatPieces[ i ] === 'd' || formatPieces[ i ] === 'dd' ) { day = datePieces[ i ]; if ( day.length < 2 ) { day = '0' + day; } } } d = Date.parse( year + '-' + month + '-' + day ); } else { d = jQuery.datepicker.parseDate( dateFormat, thisVal ); } return d; } function getSiblingField( field ) { if ( field.triggerField === undefined ) { return null; } var fields = null, container = field.triggerField.closest( '.frm_repeat_sec, .frm_repeat_inline, .frm_repeat_grid' ), repeatArgs = getRepeatArgsFromFieldName( field.triggerField.attr( 'name' ) ), siblingFieldCall = field.thisFieldCall.replace( '[id=', '[id^=' ).replace( /-"]/g, '-' + repeatArgs.repeatRow + '"]' ); if ( container.length || repeatArgs.repeatRow !== '' ) { if ( container.length ) { fields = container.find( siblingFieldCall + ',' + siblingFieldCall.replace( 'input[', 'select[' ) ); } else { fields = jQuery( siblingFieldCall ); } if ( fields === null || fields === undefined || fields.length < 1 ) { fields = uncheckedSiblingOrOutsideSection( field, container, siblingFieldCall ); } } else { // the trigger is not in the repeating section fields = getNonSiblingField( field ); } return fields; } function uncheckedSiblingOrOutsideSection( field, container, siblingFieldCall ) { var fields = null; if ( siblingFieldCall.indexOf( ':checked' ) ) { // check if the field has nothing selected, or is not inside the section var inSection = container.find( siblingFieldCall.replace( ':checked', '' ) ); if ( inSection.length < 1 ) { fields = getNonSiblingField( field ); } } else { // the field holding the value is outside of the section fields = getNonSiblingField( field ); } return fields; } function getNonSiblingField( field ) { var nonSiblingField = jQuery( field.thisFieldCall + ',' + field.thisFieldCall.replace( 'input[', 'select[' ) ); if ( ! nonSiblingField.length && 'input[' === field.thisFieldCall.substr( 0, 6 ) ) { if ( 'undefined' !== typeof field.triggerField && field.triggerField.is( 'div' ) && field.triggerField.hasClass( 'frm_form_field' ) ) { nonSiblingField = field.triggerField.find( field.thisFieldCall.replace( 'input[', 'textarea[' ) ); } else { nonSiblingField = jQuery( field.thisFieldCall.replace( 'input[', 'textarea[' ) ); } } return nonSiblingField; } function getOptionValue( thisField, currentOpt ) { var thisVal; // If current option is an other option, get other value if ( isOtherOption( thisField, currentOpt ) ) { thisVal = getOtherValueAnyField( thisField, currentOpt ); } else if ( currentOpt.type === 'checkbox' || currentOpt.type === 'radio' ) { if ( currentOpt.checked ) { // hasAttribute( 'data-frmprice' ) is a product field. thisVal = currentOpt.hasAttribute( 'data-frmprice' ) ? currentOpt.dataset.frmprice : currentOpt.value; } else { thisVal = currentOpt.dataset.off; } } else if ( currentOpt.hasAttribute( 'data-frmprice' ) ) { thisVal = currentOpt.dataset.frmprice; } else if ( isCurrencyFormatField( thisField ) ) { const fieldKey = thisField.key.replace( '[id="field_', '' ).replace( '"]', '' ); const calcRule = window.__FRMCALC?.calc?.[ fieldKey ]; thisVal = jQuery( currentOpt ).val(); if ( ! calcRule ) { return thisVal; } const resultFieldKey = thisField.total[0]; const totalField = resultFieldKey ? window.__FRMCALC?.calc?.[ resultFieldKey ] : false; if ( ! totalField || totalField.calc_type !== 'text' ) { const isNegative = '-' === thisVal.charAt( 0 ); const currency = getCurrencyFromCalcRule( calcRule ); thisVal = '' + preparePrice( thisVal, currency ); // Calling preparePrice() will remove the negative sign, so add it back if needed if ( isNegative ) { thisVal = '-' + thisVal; } } } else { thisVal = jQuery( currentOpt ).val(); } if ( thisVal === undefined ) { thisVal = ''; } return thisVal; } function isCurrencyFormatField( field ) { const fieldKey = field.key.replace( '[id="field_', '' ).replace( '"]', '' ); const isCalcField = window.__FRMCALC?.calc?.[ fieldKey ]?.is_currency; return isCalcField ? true : false; } /* Check if current option is an "Other" option (not an Other text field) */ function isOtherOption( thisField, currentOpt ) { var isOtherOpt = false; // If hidden, check for a value if ( currentOpt.type === 'hidden' ) { if ( getOtherValueLimited( currentOpt ) !== '' ) { isOtherOpt = true; } } else if ( thisField.type === 'select' ) { // If a visible dropdown field var optClass = currentOpt.className; if ( optClass && optClass.includes('frm_other_trigger') ) { isOtherOpt = true; } } else if ( thisField.type === 'checkbox' || thisField.type === 'radio' ) { // If visible checkbox/radio field if ( currentOpt.id.includes('-other_') && !currentOpt.id.includes('-otext') ) { isOtherOpt = true; } } return isOtherOpt; } /* Get the value from an "Other" text field */ /* Does NOT work for visible select fields */ function getOtherValueLimited( currentOpt ) { var otherVal = '', otherText = document.getElementById( currentOpt.id + '-otext' ); if ( otherText !== null && otherText.value !== '' ) { otherVal = otherText.value; } return otherVal; } /* Get value from Other text field */ function getOtherValueAnyField( thisField, currentOpt ) { var otherVal = 0; if ( thisField.type === 'select' ) { if ( currentOpt.type === 'hidden' ) { if ( isCurrentOptRepeating( currentOpt ) ) { // Do nothing because regular doCalculation code takes care of it } else { otherVal = getOtherValueLimited( currentOpt ); } } else { otherVal = getOtherSelectValue( currentOpt ); } } else if ( thisField.type === 'checkbox' || thisField.type === 'radio' ) { if ( currentOpt.type === 'hidden' ) { // Do nothing because regular doCalculation code takes care of it } else { otherVal = getOtherValueLimited( currentOpt ); } } return otherVal; } /* Check if current option is in a repeating section */ function isCurrentOptRepeating( currentOpt ) { var isRepeating = false, parts = currentOpt.name.split( '[' ); if ( parts.length > 2 ) { isRepeating = true; } return isRepeating; } /* Get value from Other text field in a visible dropdown field */ function getOtherSelectValue( currentOpt ) { var fields = getOtherSelects( currentOpt ); return fields.val(); } /** * Fill in the other text, and select the other option from a dropdown */ function setOtherSelectValue( thisField, value ) { var i, fields = getOtherSelects( thisField ); if ( fields.length < 1 ) { return; } fields.val( value ); for ( i = 0; i < thisField.options.length; i++ ) { if ( thisField.options[ i ].className.includes('frm_other_trigger') ) { thisField.options[ i ].selected = true; } } } function getOtherSelects( currentOpt ) { return jQuery( currentOpt ).closest( '.frm_other_container' ).find( '.frm_other_input' ); } function setOtherValueLimited( thisField, value ) { var otherText, baseId, parentInput, i = 0, idParts = thisField.id.split( '-' ); idParts.pop(); //remove the last id baseId = idParts.join( '-' ); otherText = document.querySelectorAll( '[id^=' + baseId + '-other][id$=otext]' ); if ( otherText.length > 0 ) { for ( i = 0; i < otherText.length; i++ ) { if ( otherText[ i ].value === '' ) { otherText[ i ].value = value; parentInput = document.getElementById( otherText[ i ].id.replace( '-otext', '' ) ); if ( parentInput !== null ) { parentInput.checked = true; } } } } } function savingDraftEntry( object ) { var isDraft = false, savingDraft = jQuery( object ).find( '.frm_saving_draft' ); if ( savingDraft.length ) { isDraft = savingDraft.val(); } return isDraft; } function goingToPrevPage( object ) { var goingBack = false, nextPage = jQuery( object ).find( '.frm_next_page' ); if ( nextPage.length && nextPage.val() ) { var formID = jQuery( object ).find( 'input[name="form_id"]' ).val(); var prevPage = jQuery( object ).find( 'input[name="frm_page_order_' + formID + '"]' ); if ( prevPage.length ) { prevPage = parseInt( prevPage.val() ); } else { prevPage = 0; } if ( ! prevPage || ( parseInt( nextPage.val() ) < prevPage ) ) { goingBack = true; } } return goingBack; } function afterFormSubmitted( event, form ) { // if the success message is showing, run the logic checkConditionalLogic( 'pageLoad' ); doEditInPlaceCleanUp( form ); checkFieldsOnPage(); maybeShowMoreStepsButton(); } function afterPageChanged() { checkFieldsOnPage(); addTopAddRowBtnForRepeater(); maybeDisableCheckboxesWithLimit(); // make sure this comes last so that the total is calculated only after knowing the visible fields calcProductsTotal(); maybeShowMoreStepsButton(); // calculations in a repeater need to be done again after page is changed to fix issue #3414 triggerChangeOnCalcTriggers(); maybeAddIntlTelInput( document.querySelectorAll( '.frm-intl-tel-input' ) ); initRangeInput( document.querySelectorAll( '.with_frm_style input[type=range]' ) ); initFormatFieldValueNumbers(); } function triggerChangeOnCalcTriggers() { if ( 'undefined' === typeof __FRMCALC || 'undefined' === typeof __FRMCALC.fieldKeys ) { return; } Object.values( __FRMCALC.fieldKeys ).forEach( ( key ) => { jQuery( key + ':not(label):not([type=hidden])' ).each( function() { jQuery( this ).trigger({ type: 'change', selfTriggered: true }); }); }); } /** Google Tables **/ function generateGoogleTables( graphs, graphType ) { for ( var num = 0; num < graphs.length; num++ ) { generateSingleGoogleTable( graphs[ num ], graphType ); } } function generateSingleGoogleTable( opts, type ) { google.load( 'visualization', '1.0', { packages: [ type ], callback() { compileGoogleTable( opts ); } }); } function compileGoogleTable( opts ) { var data = new google.visualization.DataTable(), showID = false; const ownerDocument = opts.document || document; if ( jQuery.inArray( 'id', opts.options.fields ) !== -1 ) { showID = true; data.addColumn( 'number', frm_js.id ); } var colCount = opts.fields.length; var type = 'string'; for ( var i = 0, l = colCount; i < l; i++ ) { var thisCol = opts.fields[ i ]; type = getGraphType( thisCol ); data.addColumn( type, thisCol.name ); } var showEdit = false; if ( opts.options.edit_link ) { showEdit = true; data.addColumn( 'string', opts.options.edit_link ); } var showDelete = false; if ( opts.options.delete_link ) { showDelete = true; data.addColumn( 'string', opts.options.delete_link ); } var col = 0; if ( opts.entries !== null ) { var entryCount = opts.entries.length; data.addRows( entryCount ); var row = 0; for ( var e = 0, len = entryCount; e < len; e++ ) { col = 0; var entry = opts.entries[ e ]; if ( showID ) { data.setCell( row, col, entry.id ); col++; } for ( var field = 0, fieldCount = colCount; field < fieldCount; field++ ) { var thisEntryCol = opts.fields[ field ]; type = getGraphType( thisEntryCol ); var fieldVal = entry.metas[ thisEntryCol.id ]; if ( type === 'number' && ( fieldVal === null || fieldVal === '' ) ) { fieldVal = 0; } else if ( type === 'boolean' ) { if ( fieldVal === null || fieldVal == 'false' || fieldVal === false ) { fieldVal = false; } else { fieldVal = true; } } data.setCell( row, col, fieldVal ); col++; } if ( showEdit ) { if ( entry.editLink !== undefined ) { data.setCell( row, col, '' + opts.options.edit_link + '' ); } else { data.setCell( row, col, '' ); } col++; } if ( showDelete ) { if ( entry.deleteLink !== undefined ) { data.setCell( row, col, '' + opts.options.delete_link + '' ); } else { data.setCell( row, col, '' ); } } row++; } } else { data.addRows( 1 ); col = 0; for ( i = 0, l = colCount; i < l; i++ ) { if ( col > 0 ) { data.setCell( 0, col, '' ); } else { data.setCell( 0, col, opts.options.no_entries ); } col++; } } var chart = new google.visualization.Table( ownerDocument.getElementById( 'frm_google_table_' + opts.options.form_id ) ); chart.draw( data, opts.graphOpts ); } /** Google Graphs **/ function generateGoogleGraphs( graphs ) { var l, i; l = graphs.length; for ( i = 0; i < l; i++ ) { generateSingleGoogleGraph( graphs[ i ]); if ( 'string' === typeof graphs[ i ].options.width && '%' === graphs[ i ].options.width.substr( -1 ) ) { addResponsiveGraphListener( graphs[ i ]); } } } function addResponsiveGraphListener( graphData ) { window.addEventListener( 'resize', () => { generateSingleGoogleGraph( graphData ); } ); } function generateSingleGoogleGraph( graphData ) { google.charts.load( 'current', { packages: [ graphData.package ] }); google.charts.setOnLoadCallback( () => { compileGoogleGraph( graphData ); }); } function compileGoogleGraph( graphData ) { var data = new google.visualization.DataTable(); data = google.visualization.arrayToDataTable( graphData.data ); const ownerDocument = graphData.document || document; var chartDiv = ownerDocument.getElementById( 'chart_' + graphData.graph_id ); if ( chartDiv === null ) { return; } var type = ( graphData.type.charAt( 0 ).toUpperCase() + graphData.type.slice( 1 ) ); if ( type !== 'Histogram' && type !== 'Table' ) { type += 'Chart'; } var chart = new google.visualization[ type ]( chartDiv ); chart.draw( data, graphData.options ); jQuery( ownerDocument ).trigger( 'frmDrawChart', [ chart, 'chart_' + graphData.graph_id, data ]); } function getGraphType( field ) { var type = 'string'; if ( field.type === 'number' ) { type = 'number'; } else if ( field.type === 'checkbox' || field.type === 'select' ) { var optCount = field.options.length; if ( field.type === 'select' && field.options[ 0 ] === '' ) { if ( field.field_options.post_field === 'post_status' ) { optCount = 3; } else { optCount = optCount - 1; } } if ( optCount == 1 ) { type = 'boolean'; } } return type; } /** Repeating Fields **/ function removeRow() { if ( ! confirmRowRemoval() ) { return; } /*jshint validthis:true */ var rowNum = jQuery( this ).data( 'key' ), sectionID = jQuery( this ).data( 'parent' ), id = 'frm_section_' + sectionID + '-' + rowNum, thisRow = jQuery( this ).parents( 'div[id^="frm_section_"]' ), fields = thisRow.find( 'input, select, textarea, .frm_html_container' ), formId = jQuery( this ).closest( 'form' ).find( 'input[name="form_id"]' ).val(); thisRow.fadeOut( 'slow', () => { const repeaterRow = thisRow[0].closest( '.frm_section_heading'); if ( repeaterRow.querySelectorAll( '.frm_repeat_sec, .frm_repeat_inline, .frm_repeat_grid' )?.length === 1 ) { repeaterRow.querySelector( '.frm_hidden_container.frm_repeat_buttons.frm_hidden' ).style.display = 'inline-block'; } thisRow.remove(); fields.each( function() { /* update calculations when a row is removed */ var fieldID; if ( this.matches( '.frm_html_container' ) ) { fieldID = getHtmlFieldID( this ); } else { fieldID = frmFrontForm.getFieldId( this, false ); } if ( this.type != 'file' ) { doCalculation( fieldID, jQuery( this ) ); } var container = 'frm_field_' + fieldID + '-' + sectionID + '-' + rowNum + '_container'; removeFromHideFields( container, formId ); // If the field is an RTE, remove the editor associated with it // this is done so that if/when the field is Added again it get initialized if ( this.classList.contains( 'wp-editor-area' ) ) { removeRichText( this.id ); } }); showAddButton( sectionID ); maybeHideRemoveButtons( sectionID ); /* eslint-disable camelcase */ if ( typeof frmThemeOverride_frmRemoveRow === 'function' ) { frmThemeOverride_frmRemoveRow( id, thisRow ); } /* eslint-enable camelcase */ jQuery( document ).trigger( 'frmAfterRemoveRow' ); }); return false; } /** * When a repeater row is removed, check for the minimum number of rows and maybe hide remove buttons. * * @param {string|number} sectionID * @returns {void} */ function maybeHideRemoveButtons( sectionID ) { var sectionContainer, repeatButtons, minRows, currentRows; sectionContainer = document.querySelector( '#frm_field_' + sectionID + '_container' ); if ( ! sectionContainer ) { return; } repeatButtons = sectionContainer.querySelector( '.frm_repeat_buttons[data-repeat-min]' ); if ( ! repeatButtons || ! repeatButtons.dataset.repeatMin ) { return; } minRows = repeatButtons.dataset.repeatMin; currentRows = document.querySelectorAll( '.frm_repeat_' + sectionID ).length; if ( currentRows <= minRows ) { hideRemoveButtons( sectionID ); } } /** * Gets the html field ID from the container in a repeater. Ex. frm_field_151-152-0_container (151 is the field ID in this case). * * @param {Array} field * @returns {Number} */ function getHtmlFieldID( field ) { var parentIDParts; parentIDParts = field.id.split( '_' ); if ( parentIDParts.length < 3 ) { return 0; } parentIDParts = parentIDParts[2]; parentIDParts = parentIDParts.split( '-' ); if ( ! parentIDParts.length ) { return 0; } return parentIDParts[0]; } function confirmRowRemoval() { if ( ! frm_js.repeaterRowDeleteConfirmation ) { return true; } return confirm( frm_js.repeaterRowDeleteConfirmation ); } function hideAddButton( sectionID ) { getRepeaterAddButtons( sectionID ).forEach( ( button ) => { button.classList.add( 'frm_hide_add_button' ); } ); } function showAddButton( sectionID ) { getRepeaterAddButtons( sectionID ).forEach( ( button ) => { button.classList.remove( 'frm_hide_add_button' ); } ); } function getRepeaterAddButtons( sectionID ) { return getFieldContainerChildren( sectionID, '.frm_add_form_row' ); } function hideRemoveButtons( sectionID ) { getRepeaterRemoveButtons( sectionID ).forEach( ( button ) => { button.classList.add( 'frm_hide_remove_button' ); } ); } function showRemoveButtons( sectionID ) { getRepeaterRemoveButtons( sectionID ).forEach( ( button ) => { button.classList.remove( 'frm_hide_remove_button' ); } ); } function getRepeaterRemoveButtons( sectionID ) { return getFieldContainerChildren( sectionID, '.frm_remove_form_row' ); } /** * @param {string|number} fieldD * @returns {HTMLElement|null} */ function getFieldContainer( fieldD ) { return document.getElementById( 'frm_field_' + fieldD + '_container' ); } function getFieldContainerChildren( fieldID, childSelector ) { var container = getFieldContainer( fieldID ); if ( ! container ) { return []; } return container.querySelectorAll( childSelector ); } function addRow() { /*jshint validthis:true */ var thisBtn, id, i, numberOfSections, lastRowIndex, stateField, state, form, data, success, error, extraParams; // If row is currently being added, leave now if ( currentlyAddingRow === true ) { return false; } // Indicate that a row is being added (so double clicking Add button doesn't cause problems) currentlyAddingRow = true; thisBtn = jQuery( this ); id = thisBtn.data( 'parent' ); i = 0; numberOfSections = jQuery( '.frm_repeat_' + id ).length; if ( numberOfSections > 0 ) { lastRowIndex = false; document.querySelectorAll( '.frm_repeat_' + id ).forEach( ( element ) => { var strippedId = element.id.replace( 'frm_section_' + id + '-', '' ), parsedId; if ( ! strippedId.length || 'i' === strippedId[0]) { return; } parsedId = parseInt( strippedId ); // strippedId should be a unique number index. // Since there is no "i" prefix, this is not an entry ID. if ( parsedId && ( false === lastRowIndex || parsedId > lastRowIndex ) ) { lastRowIndex = parsedId; } } ); if ( false === lastRowIndex ) { i = 1; } else { i = lastRowIndex + 1; } } stateField = document.querySelector( 'input[name="frm_state"]' ); state = null !== stateField ? stateField.value : ''; form = jQuery( this ).closest( 'form' ).get( 0 ); data = { action: 'frm_add_form_row', field_id: id, i, numberOfSections, nonce: frm_js.nonce, frm_state: state }; /** * Allow other add-ons to add additional data when a new repeater row is added. * * @since 6.25 * * @param {Object} data */ triggerEvent( form, 'frmBeforeNewRepeaterRow', data ); success = function( r ) { var html, item, checked, fieldID, fieldObject, repeatArgs, j, inputRanges; //only process row if row actually added if ( r.html ) { html = r.html; item = jQuery( html ).addClass( 'frm-fade-in' ); const repeaterSection = thisBtn[0].closest( '.frm_section_heading' ); const toggleContainer = repeaterSection.querySelector( '.frm_toggle_container.frm_grid_container' ); if ( toggleContainer ) { toggleContainer.append( item[0] ); } else { repeaterSection.append( item[0] ); } const repeatButtons = repeaterSection.querySelector( '.frm_hidden_container.frm_repeat_buttons.frm_hidden' ); if ( repeatButtons ) { repeatButtons.style.display = 'none'; } inputRanges = item[0].querySelectorAll( 'input[type=range]' ); for ( j = 0; j < inputRanges.length; j++ ) { handleSliderEvent.call( inputRanges[j]); } if ( r.is_repeat_limit_reached ) { hideAddButton( id ); } if ( ! r.passes_repeat_min_check ) { hideRemoveButtons( id ); } else { showRemoveButtons( id ); } checked = [ 'other' ]; repeatArgs = { repeatingSection: id.toString(), repeatRow: i.toString() }; // hide fields with conditional logic jQuery( html ).find( 'input, select, textarea' ).each( function() { // Readonly dropdown fields won't have a name attribute if ( this.name === '' ) { return true; } if ( this.type === 'file' ) { fieldID = this.name.replace( 'file', '' ).split( '-' )[0]; } else { fieldID = this.name.replace( 'item_meta[', '' ).split( ']' )[ 2 ].replace( '[', '' ); } if ( jQuery.inArray( fieldID, checked ) == -1 ) { if ( this.id === false || this.id === '' ) { return; } fieldObject = jQuery( '#' + this.id ); checked.push( fieldID ); hideOrShowFieldById( fieldID, repeatArgs ); updateWatchingFieldById( fieldID, repeatArgs, 'value changed' ); // TODO: maybe trigger a change instead of running these three functions checkFieldsWithConditionalLogicDependentOnThis( fieldID, fieldObject ); checkFieldsWatchingLookup( fieldID, fieldObject, 'value changed' ); doCalculation( fieldID, fieldObject ); maybeDoCalcForSingleField( fieldObject.get( 0 ) ); } }); jQuery( html ).find( '.frm_html_container' ).each( function() { // check heading logic var fieldID = this.id.replace( 'frm_field_', '' ).split( '-' )[ 0 ]; checked.push( fieldID ); hideOrShowFieldById( fieldID, repeatArgs ); } ); maybeAddIntlTelInput( item.find( '.frm-intl-tel-input' ).get() ); if ( item.closest( '.with_frm_style' ).length > 0 ) { initRangeInput( item.find( 'input[type=range]' ).get() ); } initFormatFieldValueNumbers(); loadDropzones( repeatArgs.repeatRow ); loadSliders(); loadAutocomplete(); loadImask(); // Load rich textboxes. jQuery( html ).find( '.frm_html_container' ).each( function() { // check heading logic var fieldID = this.id.replace( 'frm_field_', '' ).split( '-' )[ 0 ]; checked.push( fieldID ); hideOrShowFieldById( fieldID, repeatArgs ); }); // Find any RTEs in the new row/repeat and initialize the editor for them // there is an assumption here that tinyMCEPreInit.mceInit[0] refers to // an RTE that was initially loaded on the page. It's a safe assumption as // adding a row/repeat wouldn't have any RTEs if there wasn't one on the // page in the first place. jQuery( html ).find( '.wp-editor-area' ).each( function() { initRichText( this.id ); }); } /* eslint-disable camelcase */ if ( typeof frmThemeOverride_frmAddRow === 'function' ) { frmThemeOverride_frmAddRow( id, r ); } /* eslint-enable camelcase */ jQuery( document ).trigger( 'frmAfterAddRow' ); handleRangeSliders(); triggerEvent( document, 'frmAfterAddRepeaterRow', { repeater: item.get(0) }); jQuery( '.frm_repeat_' + id ).each( function( i ) { this.style.zIndex = 999 - i; }); currentlyAddingRow = false; }; error = function() { currentlyAddingRow = false; }; extraParams = { dataType: 'json' }; postToAjaxUrl( form, data, success, error, extraParams ); return false; } function triggerToggleClickOnSpace( e ) { if ( 32 === e.which ) { this.click(); } } function removeRichText( id ) { tinymce.EditorManager.execCommand( 'mceRemoveEditor', true, id ); } function initRichText( id ) { var key = Object.keys( tinyMCEPreInit.mceInit )[0], orgSettings = tinyMCEPreInit.mceInit[ key ], newValues = { selector: '#' + id, body_class: orgSettings.body_class.replace( key, id ) }, newSettings = Object.assign({}, orgSettings, newValues ); tinymce.init( newSettings ); } /***************************************************** * In-place edit ******************************************************/ function editEntry() { /*jshint validthis:true */ var $edit = jQuery( this ), entryId = $edit.data( 'entryid' ), prefix = $edit.data( 'prefix' ), postId = $edit.data( 'pageid' ), formId = $edit.data( 'formid' ), cancel = $edit.data( 'cancel' ), fields = $edit.data( 'fields' ), excludeFields = $edit.data( 'excludefields' ), startPage = $edit.data( 'startpage' ), $cont = jQuery( document.getElementById( prefix + entryId ) ), orig = $cont.html(); $cont.html( '' ); jQuery.ajax({ type: 'POST', url: getUrlForInPlaceEdit(), dataType: 'html', data: { action: 'frm_entries_edit_entry_ajax', post_id: postId, entry_id: entryId, id: formId, nonce: frm_js.nonce, fields, exclude_fields: excludeFields, start_page: startPage }, success( html ) { $cont.children( '.frm-loading-img' ).replaceWith( html ); $edit.removeClass( 'frm_inplace_edit' ).addClass( 'frm_cancel_edit' ); $edit.html( cancel ); checkConditionalLogic( 'editInPlace' ); // Make sure fields just loaded are properly bound if ( typeof frmFrontForm.fieldValueChanged === 'function' ) { jQuery( document ).on( 'change', '.frm-show-form input[name^="item_meta"], .frm-show-form select[name^="item_meta"], .frm-show-form textarea[name^="item_meta"]', frmFrontForm.fieldValueChanged ); } checkFieldsOnPage( prefix + entryId ); triggerEvent( document, 'frmInPlaceEdit' ); } }); return false; } function getUrlForInPlaceEdit() { var url = frm_js.ajax_url, queryParams = getQueryParamsForInPlaceEdit(); Array.prototype.forEach.call( Object.keys( queryParams ), ( queryParamKey ) => { url += -1 === url.indexOf( '?' ) ? '?' : '&'; url += queryParamKey + '=' + queryParams[ queryParamKey ]; } ); return url; } function getQueryParamsForInPlaceEdit() { var queryParams, queryString, pairs, keysToSkip, length, i, pair, key, value; queryParams = {}; queryString = window.location.search.substring( 1 ); pairs = queryString.split( '&' ); keysToSkip = [ 'action', 'start_page', 'nonce', 'post_id', 'entry_id', 'id', 'fields', 'doing_wp_cron' ]; length = pairs.length; for ( i = 0; i < length; i++ ) { pair = pairs[ i ].split( '=' ); key = decodeURIComponent( pair[0]); if ( -1 !== keysToSkip.indexOf( key ) ) { continue; } value = decodeURIComponent( pair[1]); // Check if the key already exists in the object. if ( queryParams.hasOwnProperty( key ) ) { // If it's an array, push the new value. if ( Array.isArray( queryParams[ key ]) ) { queryParams[ key ].push( value ); } else { // If it's not an array, convert it to an array and push the new value. queryParams[ key ] = [ queryParams[ key ], value ]; } } else { // If the key doesn't exist, create a new entry. queryParams[ key ] = value; } } return queryParams; } function cancelEdit( event ) { /*jshint validthis:true */ event.preventDefault(); const $cancelLink = jQuery( this ); const prefix = $cancelLink.data( 'prefix' ); const entryId = $cancelLink.data( 'entryid' ); const $cont = jQuery( document.getElementById( prefix + entryId ) ); $cont.children( '.frm_forms' ).replaceWith( '' ); $cont.children( '.frm_orig_content' ).fadeIn( 'slow' ).removeClass( 'frm_orig_content' ); switchCancelToEdit( $cancelLink ); } /** * Switch a Cancel Link back to Edit * * @since 2.03.08 * * @param {object} $link */ function switchCancelToEdit( $link ) { var label = $link.data( 'edit' ); $link.removeClass( 'frm_cancel_edit' ).addClass( 'frm_inplace_edit' ); $link.html( label ); } function deleteEntry() { /*jshint validthis:true */ var entryId, prefix, $link = jQuery( this ), confirmText = $link.data( 'deleteconfirm' ); if ( confirm( confirmText ) ) { entryId = $link.data( 'entryid' ); prefix = $link.data( 'prefix' ); $link.replaceWith( '' ); jQuery.ajax({ type: 'POST', url: frm_js.ajax_url, data: { action: 'frm_entries_destroy', entry: entryId, nonce: frm_js.nonce }, success( html ) { if ( html.replace( /^\s+|\s+$/g, '' ) === 'success' ) { var container = jQuery( document.getElementById( prefix + entryId ) ); container.fadeOut( 'slow', () => { container.remove(); }); jQuery( document.getElementById( 'frm_delete_' + entryId ) ).fadeOut( 'slow' ); jQuery( document ).trigger( 'frmEntryDeleted', [ entryId ]); } else { jQuery( document.getElementById( 'frm_delete_' + entryId ) ).replaceWith( html ); } } }); } return false; } /** * Switch Cancel link back to Edit link after entry is updated in-place * * @since 2.03.08 * * @param {object} form */ function doEditInPlaceCleanUp( form ) { var entryIdField = jQuery( form ).find( 'input[name="id"]' ); if ( entryIdField.length ) { var link = document.getElementById( 'frm_edit_' + entryIdField.val() ); if ( isCancelLink( link ) ) { switchCancelToEdit( jQuery( link ) ); } } } /** * Check if a link is a cancel link * * @since 2.03.08 * * @param {object} link * @returns {boolean} */ function isCancelLink( link ) { return ( link !== null && link.className.includes('frm_cancel_edit') ); } /********************************************** * Ajax time field unique check *********************************************/ function loadUniqueTimeFields() { var timeFields, i, dateField; if ( typeof __frmUniqueTimes === 'undefined' ) { return; } timeFields = __frmUniqueTimes; for ( i = 0; i < timeFields.length; i++ ) { dateField = document.getElementById( timeFields[ i ].dateID ); jQuery( dateField ).on( 'change', maybeTriggerUniqueTime ); if ( '' !== dateField.value ) { jQuery( dateField ).trigger( 'change' ); } } } function maybeTriggerUniqueTime() { /*jshint validthis:true */ var timeFields = __frmUniqueTimes; for ( var i = 0; i < timeFields.length; i++ ) { if ( timeFields[ i ].dateID == this.id ) { frmProForm.removeUsedTimes( this, timeFields[ i ].timeID ); } } } /********************************************** * General Helpers *********************************************/ function checkFieldsOnPage( containerId, event ) { if ( 'undefined' === typeof event ) { event = ''; } checkPreviouslyHiddenFields(); loadDateFields(); loadCustomInputMasks(); loadSliders(); loadAutocomplete( containerId ); checkDynamicFields( event ); checkLookupFields(); // Use a timeout to avoid conflicts with Datepicker Calculationns. // If this runs right away, the Datepicker Calculation function may not be available yet. setTimeout( triggerCalc, 0 ); loadDropzones(); loadImask( containerId ); checkPasswordFields(); triggerLookupWatchUpdates(); } function triggerLookupWatchUpdates() { var i, fieldId, keys, value, $changedInput; if ( typeof __FRMLOOKUP === 'undefined' || document.querySelector( 'form input[name="id"]' ) ) { return; } keys = Object.keys( __FRMLOOKUP ); for ( i = 0; i < keys.length; i++ ) { fieldId = keys[ i ]; value = __FRMLOOKUP[ fieldId ]; if ( value.dependents.length <= 0 ) { continue; } $changedInput = jQuery( '#field_' + value.fieldKey ); if ( $changedInput.length ) { checkFieldsWatchingLookup( fieldId, $changedInput, 'value changed' ); } } } function checkPasswordFields() { var passwordFields = document.querySelectorAll( '.frm_strength_meter' ), event = document.createEvent( 'HTMLEvents' ); event.initEvent( 'keyup', true, true ); for ( var i = 0; i < passwordFields.length; i++ ) { passwordFields[ i ].dispatchEvent( event ); } } function checkPreviouslyHiddenFields() { if ( typeof __frmHideFields !== 'undefined' ) { frmProForm.hidePreviouslyHiddenFields(); } } /** * @param {string|undefined} containerId * @returns {void} */ function loadAutocomplete( containerId ) { loadChosen( containerId ); loadSlimSelect( containerId ); } /** * @param {string|undefined} chosenContainer * @returns {void} */ function loadChosen( chosenContainer ) { var opts; if ( ! jQuery().chosen ) { return; } opts = { allow_single_deselect: true, no_results_text: frm_js.no_results, search_contains: true }; if ( typeof __frmChosen !== 'undefined' ) { opts = '{' + __frmChosen + '}'; } if ( chosenContainer !== undefined ) { jQuery( '#' + chosenContainer ).find( '.frm_chzn' ).chosen( opts ); } else { jQuery( '.frm_chzn' ).chosen( opts ); } } function loadSlimSelect( containerId ) { var container, dropdowns; if ( 'undefined' === typeof SlimSelect ) { return; } if ( 'undefined' !== typeof containerId ) { container = document.getElementById( containerId ); if ( ! container ) { return; } dropdowns = container.querySelectorAll( 'select.frm_slimselect' ); } else { dropdowns = document.querySelectorAll( 'select.frm_slimselect' ); } dropdowns.forEach( ( autocompleteInput ) => { var emptyOption, allowDeselect, isMultiSelect, tabindex; if ( 'none' === autocompleteInput.style.display || autocompleteInput.classList.contains( 'ss-main' ) || autocompleteInput.classList.contains( 'ss-content' ) ) { // Skip anything that is already initialized. return; } emptyOption = autocompleteInput.querySelector( 'option[value=""]' ); allowDeselect = false; isMultiSelect = 'multiple' === autocompleteInput.getAttribute( 'multiple' ); if ( emptyOption && '' === emptyOption.textContent.trim() ) { allowDeselect = true; if ( isMultiSelect ) { emptyOption.parentElement.removeChild( emptyOption ); } else { emptyOption.setAttribute( 'data-placeholder', 'true' ); } } else if ( autocompleteInput.querySelector( 'option[data-placeholder="true"]' ) ) { allowDeselect = true; } tabindex = autocompleteInput.getAttribute( 'tabindex' ); autocompleteInput.style.color = ''; new SlimSelect({ select: autocompleteInput, settings: { placeholderText: '', searchText: frm_js.no_results, searchPlaceholder: ' ', // Avoid default "Search" placeholder. allowDeselect, closeOnSelect: ! isMultiSelect, keepOrder: true }, events: { afterOpen() { // Make some accessibility adjustments to the .ss-content pop up. var ssContent, ssContentSearchInput, ssList, label; ssContent = document.querySelector( '.ss-content[data-id="' + autocompleteInput.dataset.id + '"]' ); if ( ! ssContent ) { return; } // The listbox role should be on the .ss-list element. ssContent.removeAttribute( 'role' ); ssContentSearchInput = ssContent.querySelector( '.ss-search input' ); if ( ! ssContentSearchInput ) { return; } ssContentSearchInput.removeAttribute( 'aria-label' ); ssList = ssContent.querySelector( '.ss-list' ); if ( ssList ) { // Set a height so the list is scrollable. ssList.style.height = getHeightForSlimSelectList( ssContent ) + 'px'; ssList.setAttribute( 'role', 'listbox' ); } // The search input and list elements should have labels. label = document.querySelector( 'label[for="' + autocompleteInput.id + '"]' ); if ( label ) { ssContentSearchInput.setAttribute( 'aria-labelledby', label.id ); if ( ssList ) { ssList.setAttribute( 'aria-labelledby', label.id ); } } } } }); if ( tabindex ) { copyTabIndexAttributeToSlimSelectElement( autocompleteInput, tabindex ); } makeSlimSelectAccessibilityChanges( autocompleteInput ); maybeApplySlimSelectAutoWidth( autocompleteInput, allowDeselect ); autocompleteInput.addEventListener( 'change', function() { // This listener is required to toggle the Other input, and for conditional logic based on this field. jQuery( autocompleteInput ).trigger( 'change' ); } ); } ); } function copyTabIndexAttributeToSlimSelectElement( autocompleteInput, tabindex ) { var slimselectElement = getSlimSelectMainElement( autocompleteInput ); if ( slimselectElement ) { slimselectElement.setAttribute( 'tabindex', tabindex ); } } function getSlimSelectMainElement( autocompleteInput ) { return document.querySelector( '.ss-main[data-id="' + autocompleteInput.getAttribute( 'data-id' ) + '"]' ); } /** * @param {HTMLElement} ssContent * @returns {number} */ function getHeightForSlimSelectList( ssContent ) { var ssSearch, listHeight; listHeight = jQuery( ssContent ).outerHeight(); ssSearch = ssContent.querySelector( '.ss-search' ); if ( ssSearch ) { listHeight -= jQuery( ssSearch ).outerHeight(); } return listHeight; } /** * @since 6.8 * * @param {HTMLElement} autocompleteInput * @returns {void} */ function makeSlimSelectAccessibilityChanges( autocompleteInput ) { addAriaLabelledByToSlimSelectMainElement( autocompleteInput ); openSlimSelectOnLabelClick( autocompleteInput ); } /** * If the autocomplete input is set to "Automatic width", use the target dropdown width as reference. * * @since 6.8.4 * * @param {HTMLElement} autocompleteInput * @param {boolean} allowDeselect * @returns {void} */ function maybeApplySlimSelectAutoWidth( autocompleteInput, allowDeselect ) { var slimselectElement, extraRequiredWidth; if ( ! autocompleteInput.classList.contains( 'auto_width' ) ) { return; } slimselectElement = getSlimSelectMainElement( autocompleteInput ); if ( slimselectElement ) { // The Slim Select dropdown is a bit larger than a regular dropdown because of the "X" icon for deselecting. // If we are not showing the "X", we don't need as much extra space. extraRequiredWidth = allowDeselect ? 36 : 14; slimselectElement.style.minWidth = ( jQuery( autocompleteInput ).outerWidth() + extraRequiredWidth ) + 'px'; } } /** * @since 6.8 * * @param {HTMLElement} autocompleteInput * @returns {void} */ function addAriaLabelledByToSlimSelectMainElement( autocompleteInput ) { var label, slimselectElement; label = document.querySelector( 'label[for="' + autocompleteInput.id + '"]' ); if ( label ) { slimselectElement = getSlimSelectMainElement( autocompleteInput ); if ( slimselectElement ) { slimselectElement.setAttribute( 'aria-labelledby', label.id ); } } } /** * @since 6.8 * * @param {HTMLElement} autocompleteInput * @returns {void} */ function openSlimSelectOnLabelClick( autocompleteInput ) { var label = document.querySelector( 'label[for="' + autocompleteInput.id + '"]' ); if ( label ) { label.addEventListener( 'click', labelListener ); function labelListener() { setTimeout( () => { autocompleteInput.slim.open(); }, 0 ); } } } function isPreviousSelectedStar( input ) { return null !== input.getAttribute( 'data-frm-star-selected' ); } function loadStars() { if ( isPreviousSelectedStar( this ) ) { clearStars( this.parentElement, false ); return; } /*jshint validthis:true */ updateStars( this, true ); } function hoverStars() { /*jshint validthis:true */ var input = this.previousSibling; if ( input.checked || isPreviousSelectedStar( this ) ) { // Do not need to update stars when hovering on the current checked star. return; } updateStars( input, false ); } /** * Updates star rating. * * @param {HTMLElement} hovered Hovered or clicked input. * @param {Boolean} onClick Is true if this update is on click, false if is on hover. */ function updateStars( hovered, onClick ) { var starGroup = hovered.parentElement, stars = starGroup.children, current = parseInt( hovered.value ), starClass = 'star-rating', selectLabel = false; starGroup.className += ' frm-star-hovered'; for ( var i = 0; i < stars.length; i++ ) { if ( stars[ i ].matches( '.' + starClass ) ) { if ( selectLabel ) { stars[ i ].className += ' star-rating-hover'; } else { stars[ i ].classList.remove( 'star-rating-hover', 'star-rating-on' ); } } else { selectLabel = ( parseInt( stars[ i ].value ) <= current ); if ( onClick ) { // Remove selected attribute from all star inputs. stars[ i ].removeAttribute( 'data-frm-star-selected' ); } } } if ( onClick ) { // Add selected class to clicked star input. hovered.setAttribute( 'data-frm-star-selected', '' ); } } function unhoverStars() { /*jshint validthis:true */ var input = this.previousSibling, starGroup = input.parentElement; starGroup.classList.remove( 'frm-star-hovered' ); var stars = starGroup.children; var selected = jQuery( starGroup ).find( 'input:checked' ).attr( 'id' ); var isSelected = ''; var starClass = 'star-rating'; for ( var i = stars.length - 1; i > 0; i-- ) { if ( stars[ i ].matches( '.' + starClass ) ) { stars[ i ].classList.remove( 'star-rating-hover' ); if ( isSelected === '' && selected !== undefined && stars[ i ].getAttribute( 'for' ) == selected ) { isSelected = ' star-rating-on'; } if ( isSelected !== '' ) { stars[ i ].className += isSelected; } } } } /** * Clears star rating. * * @since 5.4 * * @param {HTMLElement} starGroup Star group element. * @param {Boolean} noClearInput Whether to clear input value or not. Default is `false`. */ function clearStars( starGroup, noClearInput ) { var labels, input; labels = starGroup.querySelectorAll( '.star-rating-on' ); if ( labels && labels.length ) { labels.forEach( ( el ) => { el.classList.remove( 'star-rating-on' ); }); } if ( ! noClearInput ) { input = starGroup.querySelector( 'input[type="radio"]:checked' ); if ( input ) { input.checked = false; input.removeAttribute( 'data-frm-star-selected' ); } } } /** * Formats a value as currency. */ function formatValueAsCurrency( value, fieldKey ) { const currency = getCurrencyFromCalcRule( __FRMCALC.calc[ fieldKey ]); return new DOMParser().parseFromString( formatCurrency( normalizeTotal( value, currency ), currency ), 'text/html' ).documentElement.textContent; } function handleSliderEvent() { if ( this.type !== 'range' ) { return; } const sliderValueEl = this.parentNode.querySelector( '.frm_range_value' ); const fieldKey = getFieldKey( this.id, this.name ); if ( window.__FRMCALC?.calc[ fieldKey ]) { sliderValueEl.textContent = formatValueAsCurrency( this.value, fieldKey ); } else { sliderValueEl.textContent = this.value; } } function loadSliders() { handleRangeSliders(); } function getCurrencyFromCalcRule( calcRule ) { return 'undefined' !== typeof calcRule.custom_currency ? calcRule.custom_currency : getCurrency( calcRule.form_id ); } function setInlineFormWidth() { var children, f, inlineForm, inlineForms = jQuery( '.frm_inline_form .frm_fields_container' ); if ( inlineForms.length ) { for ( f = 0; f < inlineForms.length; f++ ) { inlineForm = jQuery( inlineForms[ f ]); children = inlineForm.children( '.frm_form_field' ); if ( children.length <= 12 && ! fieldHasLayoutClass( children.last() ) ) { addAutoInlineLayout( inlineForm, children ); } } } } function fieldHasLayoutClass( field ) { var i, classList = field.attr( 'class' ), layoutClasses = [ 'frm_full', 'half', 'third', 'fourth', 'fifth', 'sixth', 'seventh', 'eighth' ]; if ( classList === undefined ) { return false; } for ( i = 1; i <= 12; i++ ) { if ( field.hasClass( 'frm' + i ) ) { return true; } if ( i === 12 ) { for ( var c = 0; c < layoutClasses.length; c++ ) { if ( classList.includes(layoutClasses[ c ]) ) { return true; } if ( c === layoutClasses.length - 1 ) { return false; } } } } } function addAutoInlineLayout( inlineForm, children ) { var fieldCount, colCount, i; fieldCount = children.length + 1; // include the submit button colCount = Math.max( 2, Math.ceil( 12 / fieldCount ) ); for ( i = 0; i < children.length; i++ ) { if ( ! fieldHasLayoutClass( jQuery( children[ i ]) ) ) { jQuery( children[ i ]).addClass( 'frm' + colCount ); } } inlineForm.children( '.frm_submit' ).addClass( 'frm' + colCount ); } function checkConditionalLogic( event ) { if ( typeof __frmHideOrShowFields !== 'undefined' ) { frmProForm.hideOrShowFields( __frmHideOrShowFields, event ); } else { showForm(); } } function showForm() { document.querySelectorAll( '.frm_pro_form' ).forEach( form => { jQuery( form ).fadeIn( 'slow' ); triggerEvent( form, 'frmProAfterFormFadeIn' ); } ); } function checkDynamicFields( event ) { if ( typeof __frmDepDynamicFields !== 'undefined' ) { if ( 'pageLoad' === event && typeof __frmHideOrShowFields === 'undefined' ) { clearHideFields(); } frmProForm.checkDependentDynamicFields( __frmDepDynamicFields ); } } function checkLookupFields() { if ( typeof __frmDepLookupFields !== 'undefined' ) { frmProForm.checkDependentLookupFields( __frmDepLookupFields ); } } function triggerChange( input, fieldKey ) { if ( fieldKey === undefined ) { fieldKey = 'dependent'; } if ( input.length > 1 ) { input = input.eq( 0 ); } input.trigger({ type: 'change', selfTriggered: true, frmTriggered: fieldKey }); } function loadCustomInputMasks() { if ( typeof __frmMasks === 'undefined' ) { return; } var maskFields = __frmMasks; for ( var i = 0; i < maskFields.length; i++ ) { jQuery( maskFields[ i ].trigger ).attr( 'data-frmmask', maskFields[ i ].mask ); } } // Get the section ID and repeat row from a field name function getRepeatArgsFromFieldName( fieldName ) { var repeatArgs = { repeatingSection: '', repeatRow: '' }; if ( fieldName !== undefined && isRepeatingFieldByName( fieldName ) ) { var inputNameParts = fieldName.split( '][' ); repeatArgs.repeatingSection = inputNameParts[ 0 ].replace( 'item_meta[', '' ); repeatArgs.repeatRow = inputNameParts[ 1 ]; } return repeatArgs; } function fadeOut( $remove ) { $remove.fadeOut( 'slow', () => { $remove.remove(); }); } function objectSearch( array, value ) { for ( var prop in array ) { if ( array.hasOwnProperty( prop ) ) { if ( array[ prop ] === value ) { return prop; } } } return null; } function isNumeric( obj ) { return ! Array.isArray( obj ) && ( obj - parseFloat( obj ) + 1 ) >= 0; } function checkPasswordField() { var fieldId, fieldIdSplit, checks, split, suffix, check, span; /*jshint validthis:true */ if ( this.className.includes('frm_strength_meter') ) { fieldId = this.name.substr( this.name.indexOf( '[' ) + 1 ).replace( /\]\[\d\]\[/, '-' ); if ( fieldId[ fieldId.length - 1 ] === ']' ) { fieldId = fieldId.substr( 0, fieldId.length - 1 ); } fieldIdSplit = fieldId.split( '-' ); if ( fieldIdSplit.length === 2 ) { fieldId = fieldIdSplit[1] + '-' + fieldIdSplit[0]; } checks = passwordChecks(); split = this.id.split( '-' ); suffix = split.length > 1 && ! isNaN( split[ split.length - 1 ]) ? '-' + split[ split.length - 1 ] : ''; for ( check in checks ) { span = document.getElementById( 'frm-pass-' + check + '-' + fieldId + suffix ); addOrRemoveVerifyPass( checks[ check ], this.value, span ); } } } function passwordChecks() { return { 'eight-char': /^.{8,}$/, number: /\d/, uppercase: /[A-Z]/, lowercase: /[a-z]/, 'special-char': /(?=.*[^a-zA-Z0-9])/ }; } function addOrRemoveVerifyPass( regEx, password, span ) { if ( span !== null ) { var remove = regEx.test( password ); if ( remove ) { maybeRemovePassReq( span ); } else { maybeRemovePassVerified( span ); } } } function maybeRemovePassReq( span ) { if ( span.classList.contains( 'frm-pass-req' ) ) { span.classList.remove( 'frm-pass-req' ); span.classList.add( 'frm-pass-verified' ); } } function maybeRemovePassVerified( span ) { if ( span.classList.contains( 'frm-pass-verified' ) ) { span.classList.remove( 'frm-pass-verified' ); span.classList.add( 'frm-pass-req' ); } } function checkCheckboxSelectionLimit() { /*jshint validthis:true */ var limit = parseInt( this.getAttribute( 'data-frmlimit' ) ), checked = this.checked; if ( ! limit ) { return; } var allBoxes = jQuery( this ).parents( '.frm_opt_container' ).find( 'input[type="checkbox"]' ); if ( limit >= allBoxes.length ) { return; } var checkedBoxes = allBoxes.filter( function() { return this.checked; }); if ( checked ) { if ( checkedBoxes.length >= limit ) { allBoxes.filter( function() { return ! this.checked; }).attr( 'disabled', 'disabled' ); } } else { const checkboxesLimitNotReached = allBoxes.filter( function() { return ! this.getAttribute( 'data-max-reached' ); }); checkboxesLimitNotReached.prop( 'disabled', false ); } } /** * Set up 'Add Row' buttons that will show up when all repeated fields have been removed. * Do it this way while we await CSS4 selectors that can do sth like .frm_repeat_sec:first-of-class. * * @since 4.02.01 * @return {void} */ function addTopAddRowBtnForRepeater() { jQuery( '.frm_section_heading:has(div[class*="frm_repeat_"])' ).each( function() { const firstRepeatedSection = jQuery( this ).find( 'div[class*="frm_repeat_"]' ).first()[0]; const addButtonWrapper = document.createElement( 'div' ); addButtonWrapper.classList.add( 'frm_form_field', 'frm_hidden_container', 'frm_repeat_buttons', 'frm_hidden' ); const addButton = firstRepeatedSection.querySelector( '.frm_add_form_row' ); if ( addButton ) { addButtonWrapper.append( addButton.cloneNode( true ) ); } const buttonContainer = firstRepeatedSection.closest( '.frm_toggle_container.frm_grid_container' ); if ( buttonContainer ) { buttonContainer.prepend( addButtonWrapper ); } else { firstRepeatedSection.parentNode.insertBefore( addButtonWrapper, firstRepeatedSection ); } }); } function maybeDisableCheckboxesWithLimit() { // Disable unchecked ones after form submit error if limit is already reached. jQuery( 'input[type="checkbox"][data-frmlimit]:not(:checked)' ).each( function() { var limit = parseInt( this.getAttribute( 'data-frmlimit' ) ); if ( ! limit ) { return; } var allBoxes = jQuery( this ).parents( '.frm_opt_container' ).find( 'input[type="checkbox"]' ); if ( limit >= allBoxes.length ) { // Unreasonable limit return; } var checkedBoxes = allBoxes.filter( function() { return this.checked; }); if ( limit > checkedBoxes.length ) { return; } this.setAttribute( 'disabled', 'disabled' ); }); } /** * Check to make sure sure the quantity field value is within the min and max values. * * @param {HTMLElement} input * @return {number} */ function checkQuantityFieldMinMax( input ) { if ( '' === input.value ) { // Leave the value if it is empty. return 0; } const val = parseFloat( input.value ? input.value.trim() : 0 ); if ( isNaN( val ) ) { return 0; } let max = input.hasAttribute( 'max' ) ? parseFloat( input.getAttribute( 'max' ) ) : 0; let min = input.hasAttribute( 'min' ) ? parseFloat( input.getAttribute( 'min' ) ) : 0; max = isNaN( max ) ? 0 : max; min = isNaN( min ) ? 0 : ( min < 0 ? 0 : min ); if ( val < min ) { input.value = min; return min; } if ( 0 !== max && val > max ) { input.value = max; return max; } return val; } /** * @since 4.04.01 */ function setHiddenProduct( input ) { input.setAttribute( 'data-frmhidden', '1' ); triggerChange( jQuery( input ) ); } /** * @since 4.04.01 */ function setHiddenProductContainer( container ) { if ( container.innerHTML.includes('data-frmprice') ) { jQuery( container ).find( 'input[data-frmprice], select:has([data-frmprice])' ).attr( 'data-frmhidden', '1' ); } } /** * @since 4.04.01 */ function setShownProduct( input ) { var wasHidden = input.getAttribute( 'data-frmhidden' ); if ( wasHidden !== null ) { input.removeAttribute( 'data-frmhidden' ); triggerChange( jQuery( input ) ); } } /** * @since 4.04 */ function calcProductsTotal( e ) { var formTotals = [], totalFields; if ( typeof __FRMCURR === 'undefined' ) { return; } if ( undefined !== e && 'undefined' !== typeof e.target && ( 'keyup' === e.type || 'change' === e.type ) ) { // an event has been fired var el = e.target; if ( el.hasAttribute( 'data-frmprice' ) && el instanceof HTMLInputElement && 'text' === el.type ) { // user-defined product el.setAttribute( 'data-frmprice', el.value.trim() ); } } totalFields = jQuery( '[data-frmtotal]' ); if ( ! totalFields.length ) { return; } totalFields.each( function() { var currency, formId, formatted, total = 0, totalField = jQuery( this ), $form = totalField.closest( 'form' ), isRepeatingTotal = isRepeatingFieldByName( this.name ); if ( ! $form.length ) { return; } formId = $form.find( 'input[name="form_id"]' ).val(); currency = getCurrency( formId ); if ( formTotals[ formId ] !== undefined && ! isRepeatingTotal ) { total = formTotals[ formId ]; } else { $form.find( 'input[data-frmprice],select:has([data-frmprice])' ).each( function() { var quantity, $this, price = 0, isUserDef = false, isSingle = false; // total fields inside repeaters are for their corresponding rows only. if ( isRepeatingTotal && ! isRepeatingWithTotal( this, totalField[0]) ) { return; } if ( this.hasAttribute( 'data-frmhigherpg' ) || isProductFieldHidden( this ) ) { return; } if ( this.tagName === 'SELECT' ) { if ( this.selectedIndex !== -1 ) { price = this.options[ this.selectedIndex ].getAttribute( 'data-frmprice' ); } } else { isUserDef = 'text' === this.type; isSingle = 'hidden' === this.type; $this = jQuery( this ); if ( ( ! isUserDef && ! isSingle ) && ! $this.is( ':checked' ) ) { return; } price = this.getAttribute( 'data-frmprice' ); } if ( ! price ) { price = 0; } else { price = preparePrice( price, currency ); quantity = getQuantity( isUserDef, this ); price = parseFloat( quantity ) * parseFloat( price ); } if ( 'true' === this.getAttribute( 'data-frmdiscount' ) ) { price = price * -1; } total += price; }); if ( ! isRepeatingTotal ) { formTotals[ formId ] = total; } } total = isNaN( total ) ? 0 : total; // Set a decimal separator for currency if no default for it currency.decimal_separator = currency.decimal_separator.trim(); // first remove unnecessary space(s) if ( ! currency.decimal_separator.length ) { currency.decimal_separator = '.'; } // Round to the currency's decimal precision so floating-point artifacts // like 189.89999999999998 aren't posted as the submitted total value. // Avoid the normalized total as it can use commas as decimal separators. totalField.val( roundTotal( total, currency ) ); // Make sure '0' is always set as '0.00' to avoid issues where // the value is interpreted as empty instead of zero. if ( '0' === totalField.val() && 2 === currency.decimals ) { totalField.val( '0.00' ); } total = normalizeTotal( total, currency ); // because of e.g. fields that might be using this field for calculations triggerChange( totalField ); total = formatCurrency( total, currency ); formatted = totalField.prev( '.frm_total_formatted' ); if ( formatted.length < 1 ) { // In case paragraphs have been added to the form. formatted = totalField.closest( '.frm_form_field' ).find( '.frm_total_formatted' ); } if ( formatted.length ) { formatted.html( total ); } }); } /** * Round total and maybe add trailing zeros so formatCurrency has a proper format to work with. * * @param {float|string} total The total amount to normalize. * @param {Object} currency The currency object containing decimal information. * @returns {string} */ function normalizeTotal( total, currency ) { const isLargeTotal = total > Number.MAX_SAFE_INTEGER; total = roundTotal( total, currency ); return maybeAddTrailingZeroToPrice( total, currency, isLargeTotal ); } function roundTotal( total, currency ) { const isLargeTotal = total > Number.MAX_SAFE_INTEGER; if ( ! isLargeTotal ) { const { decimals } = currency; total = decimals > 0 ? Math.round10( total, decimals ) : Math.ceil( total ); } return total; } /** * Format a numeric value according to the specified currency format settings. * * @since 4.05.01 * * @param {string} total The numeric value to format. * @param {Object} currency The currency formatting configuration. * @returns {string} The formatted currency string. */ function formatCurrency( total, currency ) { total = maybeAddTrailingZeroToPrice( total, currency ); if ( total.length && ( total[ total.length - 1 ] === '.' || total[ total.length - 1 ] === currency.decimal_separator ) ) { total = total.substr( 0, total.length - 1 ); } total = maybeRemoveTrailingZerosFromPrice( total, currency ); total = addThousands( total, currency ); const leftSymbol = currency.symbol_left ? ( currency.symbol_left + currency.symbol_padding ) : ''; const rightSymbol = currency.symbol_right ? ( currency.symbol_padding + currency.symbol_right ) : ''; return leftSymbol + total + rightSymbol; } /** * @since 5.0.15 */ function maybeRemoveTrailingZerosFromPrice( total, currency ) { var split = total.split( currency.decimal_separator ); if ( 2 !== split.length || split[1].length <= currency.decimals ) { return total; } if ( 0 === currency.decimals ) { return split[0]; } return split[0] + currency.decimal_separator + split[1].substr( 0, currency.decimals ); } /** * @since 5.0.10 */ function addRteRequiredMessages() { var keys, length, index, key, field; if ( 'undefined' === typeof __FRMRTEREQMESSAGES ) { return; } keys = Object.keys( __FRMRTEREQMESSAGES ); length = keys.length; for ( index = 0; index < length; ++index ) { key = keys[ index ]; field = document.getElementById( key ); if ( field ) { field.setAttribute( 'data-reqmsg', __FRMRTEREQMESSAGES[ key ]); } } } /** * @since 4.04.01 * * This function is most suited for (product) fields that are * on their own page i.e. not hidden (in a multi-paged form). * * As for fields that are conditionally hidden - themselves or * their parent - and are not on their own page, the PHP side * handles them well; either their HTML is not included on the * page at all (e.g. fields in a conditionally hidden section) * or their value is empty & price is thus 0, so no worries here. */ function isProductFieldHidden( input ) { return input.getAttribute( 'data-frmhidden' ) !== null; } /** * @since 4.04 */ function isRepeatingWithTotal( input, total ) { // .+ is safer than d+ because field keys might be used at times. var regex = /item_meta\[.+?\]\[.+?\]/; return isRepeatingFieldByName( input.name ) && ( total.name.match( regex )[0] === input.name.match( regex )[0]); } /** * @since 4.04 */ function getCurrency( formId ) { if ( typeof __FRMCURR !== 'undefined' && __FRMCURR[ formId ] !== undefined ) { return __FRMCURR[ formId ]; } } /** * @since 4.04 */ function getQuantity( isUserDef, field ) { var quantity, quantityFields, isRepeating, fieldID, $this = jQuery( field ); fieldID = frmFrontForm.getFieldId( field, false ); if ( ! fieldID ) { return 0; } isRepeating = isRepeatingFieldByName( field.name ); if ( isRepeating ) { // .+ is safer than d+ because field keys might be used at times. var match = field.name.match( /item_meta\[.+?\]\[.+?\]/ ); if ( null === match ) { // very unlikely though return 0; } // we may need this in getQuantityFields $this.nameMatch = match[0]; } quantity = getQuantityField( $this, fieldID, isRepeating ); if ( quantity ) { quantity = checkQuantityFieldMinMax( quantity ); } else { quantityFields = getQuantityFields( $this, isRepeating ); if ( 1 === quantityFields.length && '' === quantityFields[0].getAttribute( 'data-frmproduct' ).trim() ) { quantity = checkQuantityFieldMinMax( quantityFields[0]); } else { // If there is no quantity field, assume 1. quantity = 1; } } if ( 0 === quantity && isUserDef ) { // only user-defined fields may not have attached quantity fields quantity = 1; } return quantity; } function getQuantityField( elementObj, fieldID, isRepeating ) { var quantity, quantityFields = elementObj.closest( 'form' ).find( '[data-frmproduct]' ); fieldID = fieldID.toString(); quantityFields.each( function() { var ids; if ( isRepeating && -1 === this.name.indexOf( elementObj.nameMatch ) ) { return true; } ids = JSON.parse( this.getAttribute( 'data-frmproduct' ).trim() ); if ( '' === ids ) { return true; } // convert to array if necessary cos of existing fields that are already using single product fields ids = 'string' === typeof ids ? [ ids.toString() ] : ids; if ( ids.includes(fieldID) ) { quantity = this; return false; } }); return quantity; } /** * @since 4.04 */ function getQuantityFields( elementObj, isRepeating ) { var quantityFields; if ( isRepeating ) { quantityFields = elementObj.closest( 'form' ).find( '[name^="' + elementObj.nameMatch + '"]' + '[data-frmproduct]' ); } else { // make sure the search is form-based (i.e. per form) cos there could be more than 1 form on the page // not([id*="-"]) means : not inside a repeater quantityFields = elementObj.closest( 'form' ).find( '[data-frmproduct]:not([id*="-"])' ); } return quantityFields; } /** * @since 4.04 */ function preparePrice( price, currency ) { var matches; if ( ! price ) { return 0; } price = price + ''; // convert to string just to be sure const regex = getRegexForPrice( currency ); matches = price.match( regex ); if ( null === matches ) { return 0; } price = matches.length ? matches[ matches.length - 1 ] : 0; price = price.trim(); // Fix issues with parsing Fr.15.00. The regex catches .15.00. // This checks for the leading decimal and removes it. if ( currency.decimal_separator === '.' && 3 === price.split( '.' ).length && price[0] === '.' ) { price = price.substr( 1 ); } if ( price ) { price = maybeUseDecimal( price, currency ); price = price.split( currency.thousand_separator ).join( '' ).replace( currency.decimal_separator, '.' ); } return price; } /** * @param {Object} currency The currency object. * @return {RegExp} */ function getRegexForPrice( currency ) { let regexString = '[0-9,.'; if ( currency.thousand_separator !== '.' && currency.thousand_separator !== ',' ) { regexString += currency.thousand_separator; } if ( currency.decimal_separator !== '.' && currency.decimal_separator !== ',' ) { regexString += currency.decimal_separator; } regexString += ']*\\.?\\,?[0-9]+'; return new RegExp( regexString, 'g' ); } /** * @since 4.04 */ function maybeUseDecimal( amount, currency ) { var usedForDecimal, amountParts; if ( currency.thousand_separator == '.' ) { amountParts = amount.split( '.' ); usedForDecimal = ( 2 == amountParts.length && 2 == amountParts[1].length ); if ( usedForDecimal ) { amount = amount.replace( '.', currency.decimal_separator ); } } return amount; } /** * Add trailing zeros to a price if necessary and replace the decimal separator. * * @since 4.04 * * @param {number|string} price The price to format. * @param {Object} currency The currency object containing the decimal separator. * @param {boolean} [force=false] Whether to force processing even if the price is not a number. * @returns {string} */ function maybeAddTrailingZeroToPrice( price, currency, force = false ) { if ( 'number' !== typeof price && ! force ) { return price; } price += ''; // first convert to string var pos = price.indexOf( '.' ); if ( pos === -1 ) { price = price + '.'; for ( let n = 0; n < currency.decimals; ++n ) { price += '0'; } } else { const decimalsString = price.substring( pos + 1 ); if ( decimalsString.length < currency.decimals ) { if ( decimalsString.length < 2 ) { price += '0'; } for ( let n = 2; n < currency.decimals; ++n ) { price += '0'; } } } return price.replace( '.', currency.decimal_separator ); } /** * Format a numeric string by adding thousand separators. * * @since 4.04.04 * * @param {string|number} total The numeric value to format. * @param {Object} options Formatting options. * @param {string} options.decimal_separator Character used as decimal separator. * @param {string} options.thousand_separator Character used as thousand separator. * * @returns {string} */ function addThousands( total, { decimal_separator, thousand_separator } ) { const split = decimal_separator === '' ? [ total.toString() ] : total.split( decimal_separator ); if ( thousand_separator ) { split[0] = split[0].replace( /\B(?=(\d{3})+(?!\d))/g, thousand_separator ); } return split.join( decimal_separator ); } /** * @since 5.1 */ function setAutoHeightForTextArea() { document.querySelectorAll( '.frm-show-form textarea' ).forEach( ( element ) => { var minHeight, callback; if ( element.dataset.autoGrow === undefined || element.getAttribute( 'frm-autogrow' ) ) { return; } minHeight = getElementHeight( element ); element.style.overflowY = 'hidden'; element.style.transition = 'none'; callback = function() { adjustHeight( element, minHeight ); }; callback(); element.addEventListener( 'input', callback ); if ( shouldCallResizeCallback() ) { window.addEventListener( 'resize', callback ); } document.addEventListener( 'frmShowField', callback ); element.setAttribute( 'frm-autogrow', 1 ); } ); } /** * Returns true if window resize callback should be called. * * Avoid this resize callback on Android and iPhone devices because it causes shaking issues. See issue #5349 for more information. * * @since 6.16.1 * * @return {Boolean} */ function shouldCallResizeCallback() { if ( ! ( 'userAgent' in navigator ) ) { return true; } const userAgent = navigator.userAgent.toLowerCase(); return !userAgent.includes('android') && !userAgent.includes('iphone'); } function getElementHeight( element ) { var clone, container, height; clone = element.cloneNode( true ); clone.style.position = 'absolute'; clone.style.left = '-10000px'; clone.style.top = '-10000px'; container = jQuery( element ).closest( '.frm_forms' ).get( 0 ); container.append( clone ); height = clone.clientHeight; container.removeChild( clone ); return height; } /** * @since 5.1 */ function adjustHeight( el, minHeight ) { if ( minHeight >= el.scrollHeight ) { return; } el.style.height = 0; el.style.height = Math.max( minHeight, el.scrollHeight ) + 'px'; } function updateContentLength() { function onChange( e ) { var length, max, type, messageEl = e.target.nextElementSibling, countEl = messageEl.querySelector( 'span' ); if ( ! countEl ) { return; } type = messageEl.getAttribute( 'data-max-type' ); max = parseInt( messageEl.getAttribute( 'data-max' ) ); if ( 'word' === type ) { length = e.target.value.split( /\s+/ ).filter( ( word ) => { return word; } ).length; } else { length = e.target.value.length; } countEl.innerText = length; messageEl.classList.toggle( 'frm_limit_error', length > max ); } document.addEventListener( 'input', function( e ) { var target; // loop parent nodes from the target to the delegation node for ( target = e.target; target && target != this; target = target.parentNode ) { if ( target.matches( 'textarea' ) && target.nextElementSibling && target.nextElementSibling.matches( '.frm_pro_max_limit_desc' ) ) { onChange( e ); break; } } }, false ); } /** * Triggers custom event. * * @since 5.1 * @since 5.3.3 Added the third parameter. * * @param {HTMLElement} element Element. * @param {String} eventType Event name. * @param {Object} data Data to add to the event. */ function triggerEvent( element, eventType, data ) { if ( 'function' === typeof frmFrontForm.triggerCustomEvent ) { frmFrontForm.triggerCustomEvent( element, eventType, data ); } } /** * Handles start over button. * * @since 5.4 */ function startOverButton() { /** * Gets all field inputs. * * @param {HTMLElement} formEl Form element. * @return {Array} */ function getInputs( formEl ) { return getInputsInFieldOnPage( formEl ); } /** * Resets field inputs. * * @param {HTMLElement} formEl Form element. */ function resetInputs( formEl ) { var inputs = getInputs( formEl ); /** * Resets a single repeater. * * @param {HTMLElement} repeatBtns Repeater buttons. */ function resetRepeater( repeatBtns ) { var repeater, items; repeater = repeatBtns.parentElement; items = repeater.querySelectorAll( '.frm_repeat_sec, .frm_repeat_inline, .frm_repeat_grid' ); if ( ! items.length ) { // Add the first row if all rows are deleted. currentlyAddingRow = false; repeatBtns.querySelector( '.frm_add_form_row' ).click(); } else if ( items.length > 1 ) { // Remove all rows except the first one. items.forEach( ( item, index ) => { if ( index ) { item.parentElement.removeChild( item ); } else { item.querySelector( '.frm_add_form_row' ).classList.remove( 'frm_hide_add_button' ); } }); } } // Remove repeater sections. formEl.querySelectorAll( '.frm_section_heading > .frm_repeat_buttons' ).forEach( resetRepeater ); // Clear field value. clearValueForInputs( inputs, '', true ); // Remove "disabled" attribute added by limit selections option. inputs.forEach( ( input ) => { if ( input.disabled && input.getAttribute( 'data-frmlimit' ) ) { input.removeAttribute( 'disabled' ); } }); } /** * Checks if given form is a multiple pages form. * * @param {Number} formId Form Id. * @return {Boolean} */ function isMultiPagesForm( formId ) { return document.getElementById( 'frm_page_order_' + formId ) || document.querySelector( '#frm_form_' + formId + '_container input[name="frm_next_page"]' ); } /** * Reloads form. * * @param {Number} formId Form ID. * @param {HTMLElement} formEl Form element. */ function reloadForm( formId, formEl ) { const stateField = document.querySelector( 'input[name="frm_state"]' ); const state = null !== stateField ? stateField.value : ''; formEl.classList.add( 'frm_loading_form' ); postToAjaxUrl( formEl, { action: 'frm_load_form', form: formId, _ajax_nonce: frm_js.nonce, frm_state: state }, ( response ) => { var idValueMapping; if ( ! response.success ) { console.warn( response ); return; } idValueMapping = getDefaultValuesFromForm( formEl ); jQuery( formEl.closest( '.frm_forms' ) ).replaceWith( response.data ); setDefaultValues( idValueMapping ); maybeShowMoreStepsButton(); if ( 'undefined' !== typeof __frmAjaxDropzone ) { window.__frmDropzone = __frmAjaxDropzone; } checkConditionalLogic( 'pageLoad' ); checkFieldsOnPage( 'frm_form_' + formId + '_container' ); triggerCompletedEvent( formId ); }, ( response ) => { console.warn( response ); } ); } /** * Gets the default values of given form. * * @param {HTMLElement} formEl Form element. * @return {Object} Object with keys are input IDs, values are the default values. */ function getDefaultValuesFromForm( formEl ) { var inputs, values = {}; inputs = formEl.querySelectorAll( '[data-frmval]' ); inputs.forEach( ( input ) => { values[ input.id ] = input.getAttribute( 'data-frmval' ); }); return values; } /** * Sets default values of fields in given form from the id value mapping built before. * * @param {Object} idValueMapping Object with keys are input IDs, values are the default values. */ function setDefaultValues( idValueMapping ) { Object.keys( idValueMapping ).forEach( ( id ) => { var input = document.getElementById( id ); if ( ! input ) { return; } input.setAttribute( 'data-frmval', idValueMapping[ id ]); if ( 'checkbox' === input.type || 'radio' === input.type ) { // Every checkbox and radio input has data-frmval, the one has value === data-frmval is the default. if ( input.value === idValueMapping[ id ]) { input.checked = true; } return; } input.value = idValueMapping[ id ]; }); } /** * Checks if form has Save draft enabled. * * @param {HTMLElement} formEl Form element. * @return {Boolean} */ function hasSaveDraft( formEl ) { return ! ! formEl.querySelector( '.frm_save_draft' ); } /** * Deletes draft version. * * @param {Number} formId Form ID. * @param {HTMLElement} formEl Form element. */ function deleteDraft( formId, formEl ) { postToAjaxUrl( formEl, { action: 'frm_delete_draft_entry', form: formId, _ajax_nonce: frm_js.nonce } ); } function onClickStartOver( e ) { e.preventDefault(); var formEl, formId, draftIdInput; formEl = e.target.closest( 'form' ); if ( ! formEl ) { return; } formId = formEl.querySelector( 'input[name="form_id"]' ).value; if ( hasSaveDraft( formEl ) ) { deleteDraft( formId, formEl ); // Clear draft inputs. draftIdInput = formEl.querySelector( 'input[name="id"]' ); if ( draftIdInput ) { draftIdInput.remove(); } formEl.querySelector( 'input[name="frm_action"]' ).value = 'create'; } if ( isMultiPagesForm( formId ) ) { reloadForm( formId, formEl ); } else { resetInputs( formEl ); triggerCompletedEvent( formId ); } } function triggerCompletedEvent( formId ) { triggerEvent( document, 'frm_after_start_over', { formId }); } document.addEventListener( 'click', function( e ) { var target; // loop parent nodes from the target to the delegation node. for ( target = e.target; target && target != this; target = target.parentNode ) { if ( target.matches( '.frm_start_over' ) ) { onClickStartOver.call( target, e ); break; } } }, false ); } function maybeShowMoreStepsButton() { var i, listWrappers, listWrapper, rootlineSteps, wrappingElementsCount, startIndex, hiddenSteps, showMoreButtonLi, showMoreButton, hiddenStepsWrapper, oldIE; listWrappers = document.getElementsByClassName( 'frm_rootline' ); copyRootlines( listWrappers ); oldIE = isOldIEVersion( 10 ); for ( i = 0; i < listWrappers.length; i++ ) { listWrapper = listWrappers[ i ]; if ( oldIE ) { // Hide the rootlines in < IE 11. listWrapper.className += ' frm_hidden'; continue; } rootlineSteps = listWrapper.children; wrappingElementsCount = countOverflowPages( rootlineSteps ); if ( ! wrappingElementsCount ) { continue; } showMoreButton = listWrapper.querySelector( '.frm_rootline_show_more_btn' ); if ( ! showMoreButton ) { continue; } showMoreButton.addEventListener( 'click', showMoreSteps ); // Show the existing "..." button. showMoreButtonLi = showMoreButton.parentNode; showMoreButtonLi.className = showMoreButtonLi.className.replace( ' frm_hidden', '' ); // Don't count the overflow step in the HTML. startIndex = rootlineSteps.length - wrappingElementsCount > 1 ? rootlineSteps.length - wrappingElementsCount - 3 : 0; hiddenSteps = [].slice.call( rootlineSteps, Math.max( startIndex, 1 ), rootlineSteps.length - 2 ); hiddenStepsWrapper = showMoreButtonLi.querySelector( '.frm_rootline_hidden_steps' ); hiddenSteps.forEach( ( hiddenStep ) => { hiddenStepsWrapper.append( hiddenStep ); }); moveRootlineTitles( hiddenStepsWrapper, listWrapper, showMoreButton ); listWrapper.insertBefore( showMoreButtonLi, listWrapper.children[ listWrapper.children.length - 1 ]); if ( listWrapper.children[ listWrapper.children.length - 1 ].className.includes('frm_current_page') ) { updateRootlineStyle( hiddenStepsWrapper ); } } } /** * Check if the browser is IE. If it matches the max or is lower, return true. * * @param int max * @returns bool */ function isOldIEVersion( max ) { var version, myNav = navigator.userAgent.toLowerCase(); version = myNav.includes('msie') ? parseInt( myNav.split( 'msie' )[1]) : false; return version !== false && max >= version; } function countOverflowPages( rootlineSteps ) { var j, wrappingElementsCount = 0; for ( j = 0; j < rootlineSteps.length; j++ ) { if ( rootlineSteps[ j ].offsetTop !== rootlineSteps[0].offsetTop && !rootlineSteps[ j ].className.includes('frm_rootline_show_hidden_steps_btn') ) { wrappingElementsCount++; } } return wrappingElementsCount; } function moveRootlineTitles( hiddenStepsWrapper, listWrapper, showMoreButton ) { var currentPageTitle, currentStepTitle, rootlineGroup, activeHiddenStepLink = hiddenStepsWrapper.querySelector( 'input:not(.frm_page_back):not(.frm_page_skip)' ); if ( activeHiddenStepLink ) { currentPageTitle = activeHiddenStepLink.parentElement.querySelector( '.frm_rootline_title' ); maybeUpdateRootlineTitles( activeHiddenStepLink.parentElement.previousElementSibling, hiddenStepsWrapper ); showMoreButton.parentElement.className += ' active'; showMoreButton.className += ' active'; } else { currentPageTitle = listWrapper.querySelector( '.frm_current_page' ).querySelector( '.frm_rootline_title' ); } if ( ! currentPageTitle ) { return; } currentStepTitle = currentPageTitle.textContent; if ( ! currentStepTitle ) { return; } // Add current step on top of rootline bar. rootlineGroup = listWrapper.closest( '.frm_rootline_group' ); showCurrentHiddenStepText( currentStepTitle ); } function showCurrentHiddenStepText( currentStepTitle ) { var rootlineCurrentStep = document.createElement( 'span' ); rootlineCurrentStep.className = 'frm_rootline_title'; rootlineCurrentStep.textContent = currentStepTitle; document.querySelector( '.frm_rootline_show_hidden_steps_btn' ).append( rootlineCurrentStep ); } function copyRootlines( listWrappers ) { var i, listWrapper, form, clone, rootlinesBackup; rootlinesBackup = {}; for ( i = 0; i < listWrappers.length; i++ ) { listWrapper = listWrappers[ i ]; form = listWrapper.closest( 'form' ); clone = listWrapper.cloneNode( true ); // Don't bake a transient disabled state (eg. mid dependent-field load) into the // snapshot used to rebuild the rootline on resize, or the "Show More" button gets // stuck disabled forever after a resize lands during that window. if ( ! form.classList.contains( 'frm_loading_form' ) ) { clone.querySelectorAll( 'input[type="button"]' ).forEach( button => button.disabled = false ); } rootlinesBackup[ form.getAttribute( 'id' ) ] = clone; } listWrappersOriginal = rootlinesBackup; } function maybeUpdateRootlineTitles( previousPageLink, hiddenStepsWrapper ) { var i; if ( previousPageLink ) { i = 0; while ( previousPageLink ) { i++; previousPageLink = previousPageLink.previousElementSibling; } updateRootlineStyle( hiddenStepsWrapper, i ); } } function updateRootlineStyle( hiddenStepsWrapper, uptoIndex ) { var rootlineTitles, rootlineTitle; if ( ! uptoIndex ) { uptoIndex = hiddenStepsWrapper.children.length; } rootlineTitles = [].slice.call( hiddenStepsWrapper.children, 0, uptoIndex ); rootlineTitles.forEach( ( el ) => { rootlineTitle = el.querySelector( '.frm_rootline_title' ); if ( rootlineTitle ) { rootlineTitle.className += ' frm_prev_page_title'; } }); } function showMoreSteps( e ) { var hiddenStepsWrapper = e.target.parentElement.querySelector( 'ul' ); if ( hiddenStepsWrapper.className.includes('frm_hidden') ) { hiddenStepsWrapper.className = hiddenStepsWrapper.className.replace( ' frm_hidden', '' ); } else { hiddenStepsWrapper.className += ' frm_hidden'; } } /** * Validates form values when submitting form. */ function validateForm() { document.addEventListener( 'frm_get_ajax_form_errors', ( event ) => { if ( ! event.frmData.formEl ) { return; } validateCheckboxMinSelections( event.frmData.formEl, event.frmData.errors ); }); } /** * @since 6.14 * * @param {HTMLElement|Object} formEl Form object. This might be a jQuery object. * @param {Array} errors * @return {void} */ function validateCheckboxMinSelections( formEl, errors ) { var checkboxes, checkedField = {}, errorMsg = frmCheckboxI18n.errorMsg.min_selections; if ( 'function' === typeof formEl.get ) { // Get the HTMLElement from a jQuery object. formEl = formEl.get( 0 ); } checkboxes = formEl.querySelectorAll( 'input[type="checkbox"][data-frmmin]:checked' ); checkboxes.forEach( ( checkbox ) => { var min, fieldEl, checkedCheckboxes, key; min = parseInt( checkbox.dataset.frmmin, 10 ); if ( ! min ) { return; } fieldEl = checkbox.closest( '.frm_form_field' ); key = fieldEl.id.replace( 'frm_field_', '' ).replace( '_container', '' ); if ( checkedField[ key ]) { // This field is processed before. return; } checkedCheckboxes = fieldEl.querySelectorAll( 'input[type="checkbox"]:checked' ); if ( ! checkedCheckboxes.length ) { // If no checkbox is checked, let required field validation do its job. return; } if ( checkedCheckboxes.length < min ) { errors[ key ] = errorMsg.replace( '%1$d', min ).replace( '%2$d', checkedCheckboxes.length ); } checkedField[ key ] = true; }); } function validateFieldValue() { document.addEventListener( 'frm_validate_field_value', ( event ) => { if ( 'object' !== typeof event.frmData.field || 'object' !== typeof event.frmData.errors ) { return; } if ( 'password' === event.frmData.field.type ) { validatePasswordStrength( event.frmData.field, event.frmData.errors ); } }); } function validatePasswordStrength( field, errors ) { var check, regex, checks; if ( 'object' !== typeof window.frm_password_checks ) { return; } if ( -1 === field.className.indexOf( 'frm_strong_pass' ) || 0 === field.id.indexOf( 'field_conf_' ) ) { return; } checks = window.frm_password_checks; for ( check in checks ) { regex = checks[ check ].regex.slice( 1, checks[ check ].regex.length - 1 ); regex = new RegExp( regex ); if ( ! regex.test( field.value ) ) { errors[ frmFrontForm.getFieldId( field ) ] = checks[ check ].message; return; } } } function maybeTriggerCalc( event ) { if ( event.persisted || ( window.performance && window.performance.getEntriesByType( 'navigation' )[0].type === 'back_forward' ) ) { triggerCalc(); } } function showMoreStepsButtonEvents() { var timeout; /** * @since 6.34 */ function onViewportResize() { var i, listWrappers, listWrapper, form; listWrappers = document.getElementsByClassName( 'frm_rootline' ); for ( i = 0; i < listWrappers.length; i++ ) { listWrapper = listWrappers[ i ]; form = listWrapper.closest( 'form' ); form.querySelector( '.frm_rootline_group' ).replaceChild( listWrappersOriginal[ form.getAttribute( 'id' ) ], listWrapper ); } clearTimeout( timeout ); timeout = setTimeout( maybeShowMoreStepsButton(), 100 ); } window.addEventListener( 'resize', onViewportResize ); // Some mobile browsers (eg. iOS Safari) only fire visualViewport's resize event, not // window's, when the address bar collapses/expands without changing the layout viewport. if ( window.visualViewport ) { window.visualViewport.addEventListener( 'resize', onViewportResize ); } } function handleShowPasswordBtn() { documentOn( 'click', '.frm_show_password_btn', ( event ) => { var input = event.target.closest( '.frm_show_password_wrapper' ).querySelector( 'input' ), button = input.nextElementSibling; if ( 'password' === input.type ) { input.type = 'text'; button.setAttribute( 'data-show-password-label', button.title ); button.title = button.getAttribute( 'data-hide-password-label' ); } else { input.type = 'password'; button.title = button.getAttribute( 'data-show-password-label' ); } button.setAttribute( 'aria-label', button.title ); }); } function handleRangeSliders() { document.querySelectorAll( '.frm_range_container' ).forEach( ( rangeContainer ) => { const rangeInput = rangeContainer.querySelector( 'input[type="hidden"]' ); if ( ! rangeInput || rangeInput.dataset.isRangeSliderInitialized ) { return; } initializeRangeSlider( rangeInput, rangeContainer ); }); } /** * Does the same as jQuery( document ).on( 'event', 'selector', handler ). * * @since 6.3.1 * * @param {String} event Event name. * @param {String} selector Selector. * @param {Function} handler Handler. * @param {Boolean|Object} options Options to be added to `addEventListener()` method. Default is `false`. */ function documentOn( event, selector, handler, options ) { if ( 'undefined' === typeof options ) { options = false; } document.addEventListener( event, function( e ) { var target; // loop parent nodes from the target to the delegation node. for ( target = e.target; target && target != this; target = target.parentNode ) { if ( target && target.matches && target.matches( selector ) ) { handler.call( target, e ); break; } } }, options ); } /** * @since 6.5.1 */ function handleElementorPopupConflicts() { var elementorPopupWrapper = document.querySelector( '.elementor-popup-modal' ); if ( null !== elementorPopupWrapper ) { // Make dropzone items clickable. elementorPopupWrapper.querySelectorAll( '.frm_dropzone' ).forEach( ( item ) => { item.classList.remove( 'dz-clickable' ); }); // Remove field chosen containers. elementorPopupWrapper.querySelectorAll( '.frm_form_field .chosen-container' ).forEach( ( chosenContainer ) => { chosenContainer.remove(); }); } loadDropzones(); loadAutocomplete(); } function getAllFormClasses( input ) { var formContainer, formClasses; formContainer = input.closest( '.with_frm_style' ); if ( ! formContainer ) { return []; } formClasses = []; Array.prototype.forEach.call( formContainer.className.split( ' ' ), ( className ) => { var trimmedClassName = className.trim(); if ( '' !== trimmedClassName && 'frm_forms' !== trimmedClassName ) { formClasses.push( trimmedClassName ); } } ); return formClasses; } function maybeAddIntlTelInput( phoneInputs ) { if ( document.body.classList.contains( 'wp-admin' ) || typeof window.intlTelInput !== 'function' ) { return; } phoneInputs.forEach( ( phoneInput ) => { const parentContainer = document.createElement( 'div' ); parentContainer.classList.add( 'frm_forms', 'with_frm_style' ); document.body.append( parentContainer ); const intlPhoneConfig = { initialCountry: 'auto', formatOnDisplay: false, dropdownContainer: parentContainer, geoIpLookup( callback ) { if ( 'function' === typeof window.fetch ) { fetch( 'https://ipapi.co/json' ) .then( function( res ) { return res.json(); } ) .then( function( data ) { callback( data.country_code ); } ) .catch( function() { callback( 'us' ); } ); } }, loadUtils: () => import( getProPluginUrl() + '/js/intl-tel-input-utils.min.js' ), }; // Allow someone to specify which countries to show in the dropdown // by adding data-only-countries in a shortcode. if ( phoneInput.dataset.onlyCountries ) { intlPhoneConfig.onlyCountries = phoneInput.dataset.onlyCountries.split( ',' ); if ( 1 === intlPhoneConfig.onlyCountries.length ) { intlPhoneConfig.initialCountry = intlPhoneConfig.onlyCountries[0]; intlPhoneConfig.allowDropdown = false; } } if ( phoneInput.dataset.countryOrder ) { intlPhoneConfig.countryOrder = phoneInput.dataset.countryOrder.split( ',' ); } const intlTelInputInstance = window.intlTelInput( phoneInput, intlPhoneConfig ); phoneInput.addEventListener( 'countrychange', function() { if ( ! this.value ) { return; } this.dispatchEvent( new Event( 'change', { bubbles: true } ) ); }); // Update the input value with the full number including the country code phoneInput.addEventListener( 'blur', function() { if ( phoneInput.dataset.numberFormat ) { phoneInput.value = intlTelInputInstance.getNumber( determineNumberFormat( phoneInput.dataset.numberFormat ) ); } else { phoneInput.value = intlTelInputInstance.getNumber(); } }); intlPhoneInputs[ phoneInput.id ] = intlTelInputInstance; }); } /** * Determines the number format based on the provided format string. * * @param {String} numberFormat The number format string used in the shortcode. * @returns {Number} The number format number. */ function determineNumberFormat( numberFormat ) { numberFormat = numberFormat.toLowerCase(); const map = { e164: 0, international: 1, national: 2, rfc3966: 3, }; return map[ numberFormat ] ?? 0; } /** * We have set 'overflow-x = clip;' initially to fix horizontal animation issue but it should be reverted * after that to avoid another style break. * * See issue #4895. */ function maybeUpdateFormsOverflowX() { document.querySelectorAll('.frm-show-form').forEach( ( slideinForm ) => { slideinForm.style['overflow-x'] = 'visible'; } ); } /** * Updates the background gradient of a range input field based on its current value. * * @since 6.12 */ function initRangeInput( rangeInputs ) { rangeInputs.forEach( ( rangeInput ) => { updateRangeInputBackground( rangeInput ); rangeInput.addEventListener( 'input', function() { updateRangeInputBackground( rangeInput ); }); rangeInput.addEventListener( 'onchange', function() { updateRangeInputBackground( rangeInput ); }); }); function updateRangeInputBackground( rangeInput ) { // Calculate the percentage of the current slider value const sliderValuePercent = ( rangeInput.value - rangeInput.min ) / ( rangeInput.max - rangeInput.min ) * 100; // Update the background gradient of the slider rangeInput.style.background = `linear-gradient(to right, var(--slider-color) 0%, var(--slider-color) ${sliderValuePercent}%, var(--slider-bar-color) ${sliderValuePercent}% 100%)`; } } /** * Initialize number formatting and input restrictions for `.frm-has-number-format` fields. */ function initFormatFieldValueNumbers() { document.querySelectorAll( '.frm-has-number-format:not([data-has-number-format-events])' ).forEach( field => { field.dataset.hasNumberFormatEvents = true; document.addEventListener( 'frmShowField', () => onShowField( field ) ); field.addEventListener( 'focus', handleFocus ); field.addEventListener( 'blur', handleBlur ); if ( field.classList.contains( 'frm_field_number' ) ) { field.addEventListener( 'keydown', restrictToNumericInput ); } triggerFormatting( field ); }); /** * Trigger formatting manually. * * @param {HTMLElement} field Field element. */ function triggerFormatting( field ) { field.dispatchEvent( new Event( 'blur', { bubbles: true } ) ); } /** * Handle the 'frmShowField' event. * * @param {HTMLElement} field Field element. */ function onShowField( field ) { if ( ! field.offsetHeight || field.value !== '' ) { return; } // Restore raw value and apply formatting when field becomes visible. const { rawValue, frmval } = field.dataset; const valueToUse = rawValue || frmval; if ( valueToUse ?? false ) { field.value = valueToUse; triggerFormatting( field ); } } /** * Restore the unformatted value when the field gains focus. * * @param {FocusEvent} event Focus event for the field. */ function handleFocus( { target: field } ) { const { rawValue } = field.dataset; if ( rawValue !== null ) { field.value = rawValue; } } /** * Save the raw value and apply formatting when the field loses focus. * * @param {FocusEvent} event Blur event for the field. */ function handleBlur( event ) { const field = event.target; const previousValue = field.value; const previousRawValue = field.getAttribute( 'data-raw-value' ) ?? ''; const fieldKey = getFieldKey( field.id, field.name ); const userTyped = field.value; field.setAttribute( 'data-raw-value', userTyped ); field.value = applyNumberFormatting( userTyped, fieldKey ); if ( previousValue !== field.value || previousRawValue !== userTyped ) { if ( event.isTrusted ) { field.dispatchEvent( new Event( 'change', { bubbles: true } ) ); } else { jQuery( field ).trigger({ type: 'change', selfTriggered: true }); } } } /** * Restrict input to valid numeric characters based on custom separators. * * @param {KeyboardEvent} event Keydown event for the field. */ function restrictToNumericInput( event ) { const { target: field, key, ctrlKey, metaKey } = event; const fieldKey = getFieldKey( field.id, field.name ); const formatConfig = getCurrencyFromCalcRule( __FRMCALC.calc?.[ fieldKey ] ) || {}; const decimalSeparator = formatConfig.decimal_separator ?? '.'; const thousandSeparator = formatConfig.thousand_separator ?? ''; const decimals = parseInt(formatConfig.decimals ?? 2, 10); const allowedKeys = [ 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Backspace', 'Delete', 'Tab', 'Home', 'End', 'Enter', ]; if ( allowedKeys.includes( key ) || ctrlKey || metaKey ) { return; } // Build regex based on allowed chars let regexPattern = '^[0-9\\+\\-'; // Only add decimal separator if decimals > 0 if ( decimals > 0 ) { regexPattern += escapeForRegex( decimalSeparator ); } if ( thousandSeparator ) { regexPattern += escapeForRegex( thousandSeparator ); } regexPattern += ']$'; const oneCharRegex = new RegExp( regexPattern ); if ( ! oneCharRegex.test( key ) ) { event.preventDefault(); return; } // Prevent decimal input if decimals = 0 if ( decimals === 0 && key === ( decimalSeparator !== '' ? decimalSeparator : '.' ) ) { event.preventDefault(); return; } // Only allow one decimal point if ( key === decimalSeparator && field.value.includes( decimalSeparator ) ) { event.preventDefault(); return; } // If user typed '+' or '-', allow only if it's the first character if ( ( key === '+' || key === '-' ) && field.selectionStart !== 0 ) { event.preventDefault(); } } /** * Format numeric substrings in the provided text based on the custom format configuration. * * @param {string} text The text containing numbers to format. * @param {string} fieldKey The field key used to retrieve the format configuration. * @returns {string} The text with formatted numbers. */ function applyNumberFormatting( text, fieldKey ) { if ( ! __FRMCALC?.calc?.[ fieldKey ] ) { return text; } const formatConfig = getCurrencyFromCalcRule( __FRMCALC.calc[fieldKey] ); const { decimal_separator: decimalSeparator = '.', thousand_separator: thousandSeparator = ',', symbol_left: symbolLeft = '', symbol_right: symbolRight = '', symbol_padding: symbolPadding = '' } = formatConfig; const escapedDecimalSeparator = escapeForRegex( decimalSeparator ); const escapedThousandSeparator = escapeForRegex( thousandSeparator ); const escapedSymbolLeft = escapeForRegex( symbolLeft ); const escapedSymbolRight = escapeForRegex( symbolRight ); const escapedSymbolPadding = escapeForRegex( symbolPadding ); // Build the regex pattern let pattern = '(? { const sign = match.startsWith( '-' ) ? '-' : ( match.startsWith( '+' ) ? '+' : '' ); const signRemoved = match.replace( /^[-+]/, '' ); const number = stripSymbolsAndFormatNumber( signRemoved, formatConfig ); const formatted = formatCurrency( normalizeTotal( number, formatConfig ), formatConfig ); return `${sign}${formatted}`; }); } /** * Strip currency symbols and convert a numeric string to a standard decimal format by removing custom separators. * * @param {string} value The numeric string. * @param {Object} config The format configuration. * @param {string} [config.decimal_separator] The custom decimal separator. * @param {string} [config.thousand_separator] The custom thousand separator. * @param {string} [config.symbol_left] The left currency symbol. * @param {string} [config.symbol_right] The right currency symbol. * @returns {string} The numeric string in standard decimal format. */ function stripSymbolsAndFormatNumber( value, config = {} ) { const { symbol_left: symbolLeft = '', symbol_right: symbolRight = '', decimal_separator: decimalSeparator = '.', thousand_separator: thousandSeparator = ',' } = config; let cleanedValue = value.trim(); // Remove left and right symbols if present if ( symbolLeft ) { cleanedValue = cleanedValue.replace( symbolLeft, '' ); } if ( symbolRight ) { cleanedValue = cleanedValue.replace( symbolRight, '' ); } cleanedValue = cleanedValue.trim(); // Remove thousand separators and replace custom decimal separator if ( thousandSeparator ) { const split = cleanedValue.split( thousandSeparator ); // Make sure that it isn't actually a decimal separator first. if ( split.length !== 2 || split[1].length > 2 ) { cleanedValue = split.join( '' ); } else { cleanedValue = split[0] + '.' + split[1]; } } if ( decimalSeparator.trim() && decimalSeparator !== '.' ) { cleanedValue = cleanedValue.split( decimalSeparator ).join( '.' ); } return cleanedValue.trim(); } /** * Escape special characters in a string for use in regular expressions. * * @param {string} str The string to escape. * @returns {string} The escaped string. */ function escapeForRegex( str ) { return str.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' ); } } /** * Initializes a range slider. * * @param {HTMLInputElement} rangeInput The input element of the range slider. * @param {HTMLElement} rangeContainer The container element of the range slider. * @return {void} */ function initializeRangeSlider( rangeInput, rangeContainer ) { const parseDataset = dataset => { const parsed = {}; for ( const [ key, value ] of Object.entries( dataset ) ) { parsed[ key ] = parseFloat( value, 10 ); } return parsed; }; const { minGap, maxGap } = parseDataset( rangeInput.dataset ); const min = parseFloat( rangeInput.min, 10 ); const max = parseFloat( rangeInput.max, 10 ); const minRange = minGap || 0; const maxRange = maxGap || max - min; // Validate inputs if ( min >= max ) { throw new Error( 'Min value must be less than max value' ); } if ( minRange <= 0 || minRange > ( max - min ) ) { throw new Error( 'Invalid minimum range value' ); } if ( maxRange < minRange || maxRange > ( max - min ) ) { throw new Error( 'Invalid maximum range value' ); } const values = rangeInput.value.split( ',' ); values[0] ||= min; values[1] ||= max; const step = parseFloat( rangeInput.step, 10 ); const sliderRange = max - min; const stepInPercent = step / sliderRange * 100; const minRangePercent = ( minRange / sliderRange ) * 100; const maxRangePercent = ( maxRange / sliderRange ) * 100; const minHandle = rangeContainer.querySelector( '.min-handle' ); const maxHandle = rangeContainer.querySelector( '.max-handle' ); const rangeElement = rangeContainer.querySelector( '.frm-slider-range' ); const minValueDisplay = rangeContainer.querySelector( '.min-value' ); const maxValueDisplay = rangeContainer.querySelector( '.max-value' ); const track = rangeContainer.querySelector( '.frm-slider-track' ); let decimalsToUse = 0; [ step, min, max, minRange, maxRange ].forEach( value => { const decimal = countDecimals( value ); if ( decimal > decimalsToUse ) { decimalsToUse = decimal; } }); const initialMinValue = Math.max( values[0], min ); const initialMaxValue = Math.min( values[1], initialMinValue + maxRange, max ); // Measured at drag start, when the slider is guaranteed to be visible. let trackWidth = 0; // Initialize positions let minPos = ( initialMinValue - min ) / sliderRange * 100; let maxPos = ( initialMaxValue - min ) / sliderRange * 100; updateHandlesPositionAndValue(); // State variables for dragging let isDragging = false; let activeHandle = null; let startX = 0; let startMinPos = 0; let startMaxPos = 0; // Setup both handles setupHandle( minHandle ); setupHandle( maxHandle ); rangeInput.addEventListener( 'change', setHandlesBasedOnValue ); function setupHandle( handle ) { handle.addEventListener( 'touchstart', handleTouchStart, { passive: false }); handle.addEventListener( 'mousedown', handleMouseDown ); } function handleTouchStart( e ) { isDragging = true; trackWidth = track.getBoundingClientRect().width; activeHandle = e.target; const touch = e.touches[0]; startX = touch.clientX; startMinPos = minPos; startMaxPos = maxPos; document.addEventListener( 'touchmove', handleTouchMove, {passive: false}); document.addEventListener( 'touchend', handleTouchEnd ); e.preventDefault(); } function handleMouseDown( e ) { isDragging = true; trackWidth = track.getBoundingClientRect().width; activeHandle = e.target; startX = e.clientX; startMinPos = minPos; startMaxPos = maxPos; document.addEventListener( 'mousemove', onMouseMove ); document.addEventListener( 'mouseup', onMouseUp ); e.preventDefault(); } function handleTouchMove( e ) { if ( ! isDragging ) { return; } const touch = e.touches[0]; const mouseEvent = new MouseEvent( 'mousemove', { clientX: touch.clientX, clientY: touch.clientY }); onMouseMove( mouseEvent, e ); e.preventDefault(); } function handleTouchEnd() { document.removeEventListener( 'touchmove', handleTouchMove ); document.removeEventListener( 'touchend', handleTouchEnd ); } function onMouseMove( e ) { if ( ! isDragging ) { return; } const deltaX = e.clientX - startX; const deltaPercent = ( deltaX / trackWidth ) * 100; const minHandleIsMoved = activeHandle === minHandle; let newMinPos, newMaxPos, hasMovedMinimumSteps; if ( minHandleIsMoved ) { newMinPos = startMinPos + deltaPercent; // The new left handle's position in % -- at the current mouse position. hasMovedMinimumSteps = Math.abs( newMinPos - minPos ) >= stepInPercent; } else { newMaxPos = startMaxPos + deltaPercent; hasMovedMinimumSteps = Math.abs( newMaxPos - maxPos ) >= stepInPercent; } if ( ! hasMovedMinimumSteps ) { return; } const movingRight = minHandleIsMoved ? newMinPos > minPos : newMaxPos > maxPos; const numberOfNewSteps = movingRight ? Math.floor( deltaPercent / stepInPercent ) : Math.ceil( deltaPercent / stepInPercent ); const newRange = activeHandle === minHandle ? maxPos - newMinPos : newMaxPos - minPos; // The distance between the right and left handles in %. const stepsDelta = numberOfNewSteps * stepInPercent; if ( activeHandle === minHandle ) { if ( newRange < minRangePercent ) { // Move the whole range to right. const tempMinPos = Math.max( startMinPos + stepsDelta, 0 ); if ( tempMinPos + minRangePercent <= 100 ) { minPos = tempMinPos; maxPos = Math.max( 0, minPos + minRangePercent ); } } else if ( newRange > maxRangePercent ) { // Move the whole range to left. minPos = Math.max( 0, startMinPos + stepsDelta ); maxPos = minPos + maxRangePercent; } else { // Normal movement within constraints minPos = Math.max( 0, startMinPos + stepsDelta ); } updateHandlesPositionAndValue(); return; } // Max handle is moved if ( newRange < minRangePercent ) { // Move the whole range to left. const tempMaxPos = Math.min( startMaxPos + stepsDelta, 100 ); if ( tempMaxPos - minRangePercent >= 0 ) { maxPos = tempMaxPos; minPos = Math.max( 0, maxPos - minRangePercent ); } } else if ( newRange > maxRangePercent ) { // Move the whole range to right. maxPos = Math.min( 100, startMaxPos + stepsDelta ); minPos = maxPos - maxRangePercent; } else { // Normal movement within constraints maxPos = Math.min( 100, startMaxPos + stepsDelta ); } updateHandlesPositionAndValue(); } function updateHandlesPositionAndValue() { minPos = parseFloat( minPos ); maxPos = parseFloat( maxPos ); // Update min and max handles positions. minHandle.style.left = `${minPos}%`; maxHandle.style.right = `${100-maxPos}%`; // Update range element. updateRangeElement(); // Update displayed values. const { currentMin, currentMax } = prepareCurrentValues(); updateDisplayedValues( currentMin, currentMax ); // Update input value. rangeContainer.querySelector( 'input' ).value = `${currentMin},${currentMax}`; } function countDecimals( num ) { const str = num.toString(); const decimalIndex = str.indexOf( '.' ); return decimalIndex === -1 ? 0 : str.length - decimalIndex - 1; } function formatSliderValue ( value ) { const fieldKey = getFieldKey( rangeInput.id, rangeInput.name ); if ( window.__FRMCALC?.calc[ fieldKey ]) { return formatValueAsCurrency( value, fieldKey ); } return value; } function updateDisplayedValues( currentMin, currentMax ) { minValueDisplay.textContent = formatSliderValue( currentMin ); maxValueDisplay.textContent = formatSliderValue( currentMax ); } function prepareCurrentValues() { let currentMin = ( min + ( minPos / stepInPercent ) * step ).toFixed( decimalsToUse ); let currentMax = ( min + ( maxPos / stepInPercent ) * step ).toFixed( decimalsToUse ); currentMin = Number( currentMin ).toPrecision().toString(); currentMax = Number( currentMax ).toPrecision().toString(); return { currentMin, currentMax }; } function onMouseUp() { isDragging = false; document.removeEventListener( 'mousemove', onMouseMove ); document.removeEventListener( 'mouseup', onMouseUp ); } function updateRangeElement() { rangeElement.style.left = `${minPos}%`; rangeElement.style.width = `${maxPos - minPos}%`; } /** * Change events are only triggered for the range slider when the default value is set * The default value is set when the field is conditionally shown. */ function setHandlesBasedOnValue() { const { minGap } = parseDataset( rangeInput.dataset ); const min = parseFloat( rangeInput.min, 10 ); const max = parseFloat( rangeInput.max, 10 ); const minRange = minGap || 0; const sliderRange = max - min; const values = rangeInput.value.split( ',' ); values[0] ||= min; values[1] ||= max; const initialMinValue = Math.max( values[0], min ); const initialMaxValue = Math.min( Math.max( values[1], initialMinValue + minRange ), max ); minPos = ( initialMinValue - min ) / sliderRange * 100; maxPos = ( initialMaxValue - min ) / sliderRange * 100; updateHandlesPositionAndValue(); } } return { init() { maybeUpdateFormsOverflowX(); maybeAddIntlTelInput( document.querySelectorAll( '.frm-intl-tel-input' ) ); initRangeInput( document.querySelectorAll( '.with_frm_style input[type=range]' ) ); initFormatFieldValueNumbers(); addEventListener( 'pageshow', maybeTriggerCalc ); jQuery( document ).on( 'frmFormComplete', afterFormSubmitted ); jQuery( document ).on( 'frmPageChanged', afterPageChanged ); jQuery( document ).on( 'frmAfterAddRow frmAfterRemoveRow', calcProductsTotal ); jQuery( document ).on( 'click', '.frm_trigger', toggleSection ); jQuery( document ).on( 'keydown', '.frm_trigger', toggleSection ); var $blankField = jQuery( '.frm_blank_field' ); if ( $blankField.length ) { $blankField.closest( '.frm_toggle_container' ).prev( '.frm_trigger' ).trigger( 'click' ); } jQuery( document ).on( 'click', '.frm_remove_link', removeFile ); jQuery( document ).on( 'frmFieldChanged', maybeCheckDependent ); jQuery( document ).on( 'keyup', 'input.frm_strength_meter', checkPasswordField ); jQuery( document ).on( 'keydown', '.frm_switch', triggerToggleClickOnSpace ); jQuery( document ).on( 'mouseenter click', '.frm-star-group input', loadStars ); jQuery( document ).on( 'mouseenter', '.frm-star-group .star-rating:not(.star-rating-readonly)', hoverStars ); jQuery( document ).on( 'mouseleave', '.frm-star-group .star-rating:not(.star-rating-readonly)', unhoverStars ); jQuery( document ).on( 'click', '.frm-show-form input[type="submit"], .frm-show-form input[name="frm_prev_page"], .frm_page_back, .frm_page_skip, .frm-show-form .frm_save_draft, .frm_prev_page, .frm_button_submit, .frm_rootline_show_hidden_steps_btn .frm_rootline_single', setNextPage ); jQuery( document ).on( 'change', '.frm_other_container input[type="checkbox"], .frm_other_container input[type="radio"], .frm_other_container select', showOtherText ); jQuery( document ).on( 'change', '.frm_switch_block input[type="checkbox"]', setToggleAriaChecked ); jQuery( document ).on( 'click', '.frm_remove_form_row', removeRow ); jQuery( document ).on( 'click', '.frm_add_form_row', addRow ); // In place edit. jQuery( document ).on( 'click', '.frm_edit_link_container a.frm_inplace_edit', editEntry ); jQuery( document ).on( 'click', '.frm_edit_link_container a.frm_cancel_edit', cancelEdit ); jQuery( document ).on( 'click', '.frm_ajax_delete', deleteEntry ); // toggle collapsible entries shortcode jQuery( '.frm_month_heading, .frm_year_heading' ).on( 'click', function() { var content = jQuery( this ).children( '.ui-icon-triangle-1-e, .ui-icon-triangle-1-s' ); if ( content.hasClass( 'ui-icon-triangle-1-e' ) ) { content.addClass( 'ui-icon-triangle-1-s' ).removeClass( 'ui-icon-triangle-1-e' ); jQuery( this ).next( '.frm_toggle_container' ).fadeIn( 'slow' ); } else { content.addClass( 'ui-icon-triangle-1-e' ).removeClass( 'ui-icon-triangle-1-s' ); jQuery( this ).next( '.frm_toggle_container' ).hide(); } }); // Elementor popup show event. Fix Elementor Popup && FF PRO fields( file upload | dynamic field ) conflicts jQuery( document ).on( 'elementor/popup/show', handleElementorPopupConflicts ); addTopAddRowBtnForRepeater(); // Checkbox Selection Limit jQuery( document ).on( 'click', 'input[type="checkbox"][data-frmlimit]', checkCheckboxSelectionLimit ); // Total field calc jQuery( document ).on( 'change', '[type="checkbox"][data-frmprice],[type="radio"][data-frmprice],[type="hidden"][data-frmprice],select:has([data-frmprice])', calcProductsTotal ); jQuery( document ).on( 'keyup change', '[data-frmproduct],[type="text"][data-frmprice]', calcProductsTotal ); jQuery( document ).on( 'frmFormComplete frmPageChanged frmInPlaceEdit frmAfterAddRow', setAutoHeightForTextArea ); maybeDisableCheckboxesWithLimit(); setInlineFormWidth(); checkConditionalLogic( 'pageLoad' ); checkFieldsOnPage( undefined, 'pageLoad' ); processPendingAjax(); addRteRequiredMessages(); setAutoHeightForTextArea(); updateContentLength(); // make sure this comes last, particularly after checkConditionalLogic & checkFieldsOnPage calcProductsTotal(); startOverButton(); maybeShowMoreStepsButton(); showMoreStepsButtonEvents(); validateFieldValue(); validateForm(); handleShowPasswordBtn(); jQuery( document ).on( 'input change', 'input[data-frmrange]', handleSliderEvent ); }, savingDraft( object ) { return savingDraftEntry( object ); }, goingToPreviousPage( object ) { return goingToPrevPage( object ); }, hideOrShowFields( ids, event ) { if ( 'pageLoad' === event ) { clearHideFields(); } var len = ids.length, repeatArgs = { repeatingSection: '', repeatRow: '' }; for ( var i = 0, l = len; i < l; i++ ) { hideOrShowFieldById( ids[ i ], repeatArgs ); if ( i == ( l - 1 ) ) { showForm(); } } }, hidePreviouslyHiddenFields() { var hiddenFields = getAllHiddenFields(), len = hiddenFields.length; for ( var i = 0, l = len; i < l; i++ ) { var container = document.getElementById( hiddenFields[ i ]); //check for submit button if ( container == null ) { container = document.querySelector( '#' + hiddenFields[ i ]); if ( container != null && hiddenFields[ i ].includes('frm_final_submit') ) { hidePreviouslyHiddenSubmitButton( hiddenFields[ i ]); continue; } } if ( container !== null ) { container.style.display = 'none'; setHiddenProductContainer( container ); } } }, submitAllowed( object ) { var formElementId = object.getAttribute( 'id' ); if ( ! isSubmitButtonOnPage( formElementId + ' .frm_final_submit' ) || goingToPrevPage( object ) || savingDraftEntry( object ) ) { return true; } var formKey = getFormKeyFromFormElementID( formElementId ); return ! isOnPageSubmitButtonHidden( formKey ); }, checkDependentDynamicFields( ids ) { var len = ids.length, repeatArgs = { repeatingSection: '', repeatRow: '' }; for ( var i = 0, l = len; i < l; i++ ) { hideOrShowFieldById( ids[ i ], repeatArgs ); } }, checkDependentLookupFields( ids ) { var fieldId, repeatArgs = { repeatingSection: '', repeatRow: '' }; for ( var i = 0, l = ids.length; i < l; i++ ) { fieldId = ids[ i ]; updateWatchingFieldById( fieldId, repeatArgs, 'value changed' ); } }, loadGoogle() { var graphs, packages, i; if ( typeof google === 'undefined' || ! google || ! google.load ) { setTimeout( frmProForm.loadGoogle, 30 ); return; } graphs = __FRMTABLES; packages = Object.keys( graphs ); for ( i = 0; i < packages.length; i++ ) { if ( packages[ i ] === 'graphs' ) { generateGoogleGraphs( graphs[ packages[ i ] ]); } else { generateGoogleTables( graphs[ packages[ i ] ], packages[ i ]); } } }, removeUsedTimes( obj, timeField ) { var $form, form, e, data, success, extraParams; $form = jQuery( obj ).parents( 'form' ).first(); form = $form.get( 0 ); e = $form.find( 'input[name="id"]' ); data = { action: 'frm_fields_ajax_time_options', time_field: timeField, date_field: obj.id, entry_id: ( e ? e.val() : '' ), date: jQuery( obj ).val(), nonce: frm_js.nonce }; success = function( opts ) { if ( null !== document.getElementById( timeField + '_H' ) ) { frmProForm.removeUsedTimesForMultipleDropdown( opts, timeField ); return; } var $timeField = jQuery( document.getElementById( timeField ) ); $timeField.find( 'option' ).prop( 'disabled', false ); if ( opts.length > 0 ) { for ( var i = 0, l = opts.length; i < l; i++ ) { $timeField.get( 0 ).querySelectorAll( 'option[value="' + opts[ i ] + '"]' ).forEach( ( option ) => { option.disabled = true; if ( option.selected ) { option.selected = false; } } ); } } }; extraParams = { dataType: 'json' }; postToAjaxUrl( form, data, success, false, extraParams ); }, removeUsedTimesForMultipleDropdown( times, timeField ) { const self = this; self.hDropdown = null; self.mDropdown = null; self.ampmDropdown = null; function getTimeDropdownsElements( timeField ) { return { h: getTimeDropdownsElements.prototype.h[ timeField ], m: getTimeDropdownsElements.prototype.m[ timeField ], ampm: getTimeDropdownsElements.prototype.ampm[ timeField ] }; } this.init = function() { if ( 'undefined' === typeof getTimeDropdownsElements.prototype.h ) { getTimeDropdownsElements.prototype.h = []; getTimeDropdownsElements.prototype.m = []; getTimeDropdownsElements.prototype.ampm = []; } if ( 'undefined' !== typeof getTimeDropdownsElements.prototype.h[ timeField ] ) { return; } getTimeDropdownsElements.prototype.h[ timeField ] = document.getElementById( timeField + '_H' ); getTimeDropdownsElements.prototype.m[ timeField ] = document.getElementById( timeField + '_m' ); getTimeDropdownsElements.prototype.ampm[ timeField ] = document.getElementById( timeField + '_A' ); if ( null === getTimeDropdownsElements( timeField ).h ) { return; } getTimeDropdownsElements( timeField ).h.addEventListener( 'change', ( event ) =>{ self.disableMinutes( event.target.value ); }); getTimeDropdownsElements( timeField ).m.addEventListener( 'change', ( event ) =>{ }); getTimeDropdownsElements( timeField ).ampm.addEventListener( 'change', () =>{ self.disableHours(); }); self.disableHours(); self.disableAmpm(); }; this.checkIfTimeIsDisabled = time => times.includes( time ); this.getActiveTime = function() { var h = getTimeDropdownsElements( timeField ).h.value || '12', m = getTimeDropdownsElements( timeField ).m.value || '00', ampm = getTimeDropdownsElements( timeField ).ampm.value || 'AM'; return h + ':' + m + ' ' + ampm; }; this.getMeridiem = function() { return getTimeDropdownsElements( timeField ).ampm.value || 'AM'; }; this.listPossibleHourTimes = function( hour ) { const minutes = []; const ampm = []; const minutesElement = getTimeDropdownsElements( timeField ).m; const ampmElement = getTimeDropdownsElements( timeField ).ampm; for ( let i = 0; i < ampmElement.options.length; i++ ) { ampm.push( ampmElement.options[ i ].value ); } for ( let i = 0; i < minutesElement.options.length; i++ ) { if ( '' !== minutesElement.options[ i ].value ) { minutes.push( minutesElement.options[ i ].value ); } } return ampm.reduce( ( array, ampm ) => { array[ ampm ] = minutes.map( ( minute ) => { return hour + ':' + minute + ' ' + ampm; }); return array; }, {} ); }; this.hourHasEmptySlots = function( hour ) { const possibleTimes = this.listPossibleHourTimes( hour ); const ampm = this.getMeridiem(); const allPossibleTimes = possibleTimes[ ampm ]; return ! allPossibleTimes.every( ( time ) => { return times.includes( time ); }); }; this.disableHours = function() { if ( null === getTimeDropdownsElements( timeField ).h ) { return; } getTimeDropdownsElements( timeField ).h.querySelectorAll( 'option' ).forEach( ( option ) => { option.disabled = ! self.hourHasEmptySlots( option.value ); }); }; this.disableMinutes = function( hour ) { if ( null === getTimeDropdownsElements( timeField ).m ) { return; } getTimeDropdownsElements( timeField ).m.querySelectorAll( 'option' ).forEach( ( option ) => { const timeString = hour + ':' + option.value + ' AM'; option.disabled = times.includes( timeString ); }); }; this.disableAmpm = function() { if ( null === getTimeDropdownsElements( timeField ).ampm ) { return; } getTimeDropdownsElements( timeField ).ampm.querySelectorAll( 'option' ).forEach( ( option ) => { if ( times.includes( option.value ) ) { option.disabled = true; } }); }; this.init(); }, changeRte( editor ) { editor.on( 'change', () => { var content = editor.getBody().innerHTML; jQuery( '#' + editor.id ).val( content ).trigger( 'change' ); }); }, addFormidableClassToDatepicker( _, options ) { if ( options.dpDiv ) { Array.prototype.forEach.call( getAllFormClasses( options.input.get( 0 ) ), ( formClass ) => { options.dpDiv.get( 0 ).classList.add( formClass ); } ); options.dpDiv.addClass( 'frm-datepicker' ); options.dpDiv.get( 0 ).setAttribute( 'is-formidable-datepicker', 1 ); options.input.get( 0 )?.classList.add( 'active' ); } return options; }, removeFormidableClassFromDatepicker( _, options ) { var dpDiv; if ( options.dpDiv ) { dpDiv = options.dpDiv.get( 0 ); dpDiv.removeAttribute( 'is-formidable-datepicker' ); options.input.get( 0 )?.classList.remove( 'active' ); // Delay it slightly so it doesn't use styling while it's fading out. setTimeout( () => { if ( dpDiv.hasAttribute( 'is-formidable-datepicker' ) ) { // Avoid removing it the class if the datepicker was immediately opened again. return; } Array.prototype.forEach.call( getAllFormClasses( options.input.get( 0 ) ), function( formClass ) { options.dpDiv.get( 0 ).classList.remove( formClass ); } ); jQuery( dpDiv ).removeClass( 'frm-datepicker' ); }, 400 ); } }, isIntlPhoneInput( field ) { var pattern = field.getAttribute( 'pattern' ); if ( '\\+?\\d{1,4}[\\s\\-]?(?:\\(\\d{1,3}\\)[\\s\\-]?)?\\d{1,4}[\\s\\-]?\\d{1,4}[\\s\\-]?\\d{1,4}$' !== pattern ) { return false; } return 'undefined' !== typeof intlPhoneInputs && 'undefined' !== typeof intlPhoneInputs[ field.id ]; }, validateIntlPhoneInput( field ) { return intlPhoneInputs[ field.id ].isValidNumber(); }, frmDatepicker: frmDatepickerPro, }; /** * Keep label floating if datepicker is open (prevents jumping when clicking date) * * @param {Event} event Focusout event * @return {void} */ function keepFloatingLabelOnDateBlur( event ) { const input = event.target.closest( '.frm_date' ); if ( input?.classList.contains( 'active' ) ) { input.closest( '.frm_inside_container' )?.classList.add( 'frm_label_float_top' ); } } } var frmProForm = frmProFormJS(); document.addEventListener( 'frmMaybeDelayFocus', ( event ) => { if ( 'object' !== typeof event.frmData || 'undefined' === typeof event.frmData.input ) { return; } const input = event.frmData.input; // Possibly delay focus. const form = input.closest( 'form' ); if ( form ) { const focusHandler = () => { input.focus(); form.removeEventListener( 'frmProAfterFormFadeIn', focusHandler ); }; form.addEventListener( 'frmProAfterFormFadeIn', focusHandler ); } } ); jQuery( document ).ready( () => { frmProForm.init(); }); if ( ! Math.round10 ) { // https://www.jacklmoore.com/notes/rounding-in-javascript/ Math.round10 = function( value, decimals ) { return Number( Math.round( value + 'e' + decimals ) + 'e-' + decimals ); }; } window.frmUpdateField = function( entryId, fieldId, value, message, num ) { jQuery( document.getElementById( 'frm_update_field_' + entryId + '_' + fieldId + '_' + num ) ).html( '' ); jQuery.ajax({ type: 'POST', url: frm_js.ajax_url, // eslint-disable-line camelcase data: { action: 'frm_entries_update_field_ajax', entry_id: entryId, field_id: fieldId, value, nonce: frm_js.nonce // eslint-disable-line camelcase }, success() { if ( message.replace( /^\s+|\s+$/g, '' ) === '' ) { jQuery( document.getElementById( 'frm_update_field_' + entryId + '_' + fieldId + '_' + num ) ).fadeOut( 'slow' ); } else { jQuery( document.getElementById( 'frm_update_field_' + entryId + '_' + fieldId + '_' + num ) ).replaceWith( message ); } } }); };